2021년 8월경에 제가 블록체인에 대해 공부한 내용들을 정리했습니다.
강의 : Klaytn 클레이튼 스마트계약과 탈중앙앱 강의(한양대학교)
set
, get
은 함수, storedData
는 상태.pragma solidity
: solidity 버전 지정.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;
}
}
minter
: 이더리움 160-bit address(주소) 타입, public이므로 다른 컨트랙트에서 minter를 읽어들일 수 있음.balances
: key-value mapping 타입. address가 key, uint가 value.pragma solidity ^0.5.6;
contract Coin {
address public minter;
mapping (address => uint) public balances;
}
Sent
: 이용자가 다른 이용자에게 coin을 전송할 때 발생시킬 이벤트.contract Coin {
event Sent(address from, address to, uint amount);
}
minter
에는 컨트랙트 배포자의 주소값(msg.sender)가 들어감.contract Coin {
constructor() public {
minter = msg.sender;
}
}
mint
함수 : receiver 주소에 amount만큼의 새로운 Coin을 부여.require
: 입력값이 true일 때만 다음으로 진행. 뒤에 string을 추가로 넣어서 에러값에 대한 리턴 처리를 할 수 있다.(아래 참고)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
: 정의된 이벤트를 발생시킴.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);
}
}
State Variables = 블록체인에 영구히 저장할 값들.
public
: 변수를 외부에서 접근 가능하게 함.contract ~~~ {
/// State Variables 예시
uint public count = 0;
address public lastParticipant;
}
external, public, internal, private
중 하나로 visibility를 설정 가능 (아래 참고)payable, view, pure
등 함수 유형을 정의 가능contract ~~~ {
/// Functions 예시
function plus() public {
count++;
lastParticipant = msg.sender;
}
}
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 = EVM 로깅을 활용한 시스템.
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 = Solidity에서 제공하지 않는 새로운 형식의 자료를 만들 때 사용.
(여러 자료를 묶어 복잡한 자료형을 만들 때 유용함.)
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 = 임의의 상수를 정의하기에 적합함.
contract Ballot {
/// Enum Types 예시
enum Status {
Open,
Closed
}
}
Solidity의 자료형은 일반 언어의 자료형과 조금씩 다른 부분들이 있다.
Address = account 주소를 표현.
Reference Types = structs, arrays, mapping과 같이 크기가 정해지지 않은 데이터를 위해 사용
string memory s = "Hello, world";
string memory t = s;
※ Value Type : 크기가 정해진 데이터 타입. Value Type 변수끼리 대입하는 경우, 새로운 메모리 영역을 만들고 거기에 값을 복사한다.
memory
: 함수 내에서 유효한 영역에 저장, 휘발성 데이터.storage
: state variable 취급. 영속적으로 저장되는 영역에 저장calldata
: external 함수 인자에 사용되는 공간storage ⇒ memory / calldata
(이 경우, memory의 값을 변경해도 storage에는 반영되지 않는다.)anything ⇒ storage
(이 경우, storage의 값이 바뀌어도 memory의 값은 바뀌지 않는다.)자바스크립트의 배열과 개념은 같으나 사용법이 많이 다르다.
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];
/// 배열에 데이터를 추가함
.push(T item);
/// 배열크기를 반환
.length
/// 배열을 삭제하지 않고, 배열을 빈 상태로 초기화하는 함수.
delete(array);
new
키워드로 선언.되도록이면 bytes를 사용할 것!
(byte[]는 배열 아이템 간 31바이트 패딩이 추가됨.)
해시테이블과 유사함.
storage 영역에만 저장 가능. 즉, State Variable로만 선언 가능.
함수 인자, public 리턴값으로 사용할 수 없음.
(함수에서는 State Variable로 선언된 Mapping Type 변수를 사용할 수만 있다.)
/// Mapping Types 선언
mapping (K => V) table;
- Contract는 불변하다. 즉, 한 번 배포한 것은 바꿀 수 없다!
=> 단, 배포 이후 변경사항을 적용할 수 있도록 Proxy 등을 쓸 수는 있다.
이 경우, Proxy에서는 특정 기능과 Contract를 서로 연결해두고, 만약 특정 기능에서 수정사항이 발생하면, 수정된 Contract를 배포한 뒤, Proxy에서 특정 기능과 새로운 Contract를 연결시키는 방식으로 작업한다. (다만 일반적이지 않다.)
- Contract는 되도록 짧게 설계하는 게 좋다.
=> 1번 원칙도 문제고, Contract가 길어지면 연산량이 많아지면서 남은 가스량을 체크해야 하는 경우가 생기기 때문에 복잡도가 크게 증가할 수 있다.
보통 1sequence를 통과하면 종료될 수 있도록 설계한다.
- 블록체인은 비싼 Storage이므로, 저장 공간에 제약이 있다는 것을 감안해야 한다.
=> 32바이트 크기의 데이터를 가장 많이 쓰며, 고정형 크기의 데이터를 잘 활용해야 한다.
Contract가 발생시키는 가스비의 경우, 계산 비용도 문제가 되지만, 그보다는 저장 비용이 더 문제가 된다.
1byte도 아껴쓰자!!!
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.
assert(bool condition)
: condition이 false인 경우 실행 중인 함수가 변경한 내역을 모두 이전 상태로 되돌림. (로직 체크에 사용)
require(bool condition)
: condition이 false인 경우 실행 중인 함수가 변경한 내역을 모두 이전 상태로 되돌림 (외부 변수 검증에 사용)
(다만 assert가 쓰여야 맞는 부분도 require를 쓸 수 있다.)
require(bool condition, string memory message)
: require(bool)과 동일, 오류 발생 시 message를 전달.
★ 주의 : 아래 함수들은 가스비가 매우 비싸니, 가급적 쓰지 말자!
따라서, 보통은 블록체인 밖에서 해시를 하고, 그 값을 블록체인에 넣는다.
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 주소).
: 크게 다를 게 없다.
/// for 구문 예시
for(uint i=0; i<repeat; i++) {
...
}
/// while 구문 예시
while(i < repeat) {
...
}
'Contract를 만든다' = 'Contract를 배포한다' or 'Contract를 Class처럼 사용한다.'
contract A {
B b;
constructor() public {
b = new B(10);
}
...
}
contract B {
uint base;
constructor(uint _base) public {
base = _base;
}
...
}
external
public
internal
private
Function Declarations = 함수에 제약을 걸어서, 정해진 scope에서 동작할 수 있도록 설정한다.
pure
view
Fallback function = Contract에 일치하는 함수가 없을 경우 실행 (no input/calldata)
contract Escrow {
event Deposited(address sender, uint amount);
/// Fallback function.
/// 함수명, 파라미터, 리턴값 다 없음 + visibility는 external.
function() external payable {
emit Deposited(msg.sender, msg.value);
}
}