336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

Solidity 개발은 기타 새로운 프로그래밍 언어나 프레임워크를 배우는 것보다 더 까다롭습니다. 국문 자료가 부족한 점도 있고, Solidity 문법을 이해하여 작성하는 것까지는 가능해도 이를 실행시켜보려면 이더리움 클라이언트를 실행하고 이 클라이언트에 JSON-RPC 콜을 보내는 방법 역시 필요합니다. 이 전체적인 과정이 깔끔하게 진행되면 좋겠지만, 이더리움이 코어/프로토콜 개발에만 몰두되어 있고 스마트 컨트랙 개발자 생태계는 상대적으로 투자가 덜 되어서 관련 툴의 완성도가 떨어지는 편입니다. 종종 사소한 실수조차 에러 메시지가 너무 일반적이어서 초심자들은 에러의 원인을 알기 어려운 경우가 있습니다.

이 글은 스마트 컨트랙의 배포와 실행 과정은 최대한 무시한 채 일단 Solidity 문법을 공부해서 작성해보고 실행해보기를 원하는 분들을 위해 작성했습니다. Truffle Framework 4 버전을 기준으로 작성되었습니다. 또한 기본적인 터미널 사용 환경과 Node.js에 익숙하다는 전제 하에 작성되었습니다.

개발 환경을 구축하기 위해 사용할 Truffle Framework는 Node.js 5 버전 이상을 요구합니다. 가이드 작성에는 8 버전을 사용했고 예제에 사용된 문법 역시 다를 수 있으니 참고하시기 바랍니다.

truffle 설치 및 프로젝트 환경 설정

npm 명령어로 truffle을 우선 설치합니다.
`npm install -g truffle`





-g 옵션으로 설치했으면 truffle 명령어를 실행 가능한 상태가 됩니다. `truffle unbox` 명령어로 예제 코드가 포함된 프로젝트를 생성합니다. 추후에 예제 코드를 제외한 상태로 프로젝트를 생성하려면 `truffle init` 명령어를 수행하면 됩니다.


프로젝트 생성이 완료되면 디렉토리와 파일들이 생성되고 친절하게 자주 사용하게 될 명령어 3개를 보여줍니다. compile, migrate, test 입니다.

contracts/ 디렉토리는 Solidity 컨트랙 코드가 있는 디렉토리입니다. `truffle compile` 명령어를 수행하면 Solidity 컴파일을 수행해서 관련 내용을 build/ 디렉토리에 저장합니다.



`truffle compile`을 수행하면 build 디렉토리가 생성되어 그 밑에 컨트랙 별로 json 파일이 생성됩니다. 이 파일을 열어보면 ABI, 바이트코드, 컴파일러, 네트워크 정보가 저장되는 것을 볼 수 있습니다. 막 컴파일한 상태에서는 networks 필드가 빈 오브젝트로 나오는데, 이는 migration 시에 네트워크에 배포하게 되면 네트워크에 배포한 정보를 저장합니다.

`truffle migrate`를 수행하려면 이더리움 클라이언트를 실행시키고 관련 설정을 먼저 해주어야 합니다.

테스트용 이더리움 노드 설정

geth나 parity 등 실제 이더리움 클라이언트를 설치하여도 무방하지만, 개발 학습용으로 적합하지 않습니다. 이 가이드에선 이더리움 노드의 동작을 시뮬레이션하는 ganache-cli(구 TestRPC)를 사용하는 방법 두 가지를 소개합니다.

`truffle develop`

truffle에 내장되어 있는 ganache-cli 실행 방법입니다. 포트 기본값이 9545 입니다.





`npm install -g ganache-cli`
`ganache-cli`


ganache-cli를 직접 설치해서 실행하는 방법입니다. 포트 기본값이 8545 이고 ganache-cli의 특정 버전을 쓰고 싶거나 특정 옵션이 필요할 때 쓰기에 적합한 방법인 것 같습니다.




실행 시 처음 보여주는 계정 10개는 100 ETH를 가지고 있습니다. Ctrl+C 등으로 프로세스가 종료되면 데이터는 모두 날아갑니다. truffle로 테스트하는 입장에서는 deploy 부터 다시 해주어야 하니 만약 데이터 저장이 필요하다면 ganache-cli의 --db 옵션을 써서 저장하여야 합니다.

이더리움 노드 설정이 완료되었다면 truffle 프로젝트 디렉토리에 있는 truffle.js를 수정해서 host와 port를 맞게 설정하면 됩니다.

truffle migrate

`truffle migrate` 명령어를 수행하면 migrations 디렉토리에 있는 스크립트를 차례대로 수행합니다. 스크립트 파일 이름에 붙어 있는 숫자 prefix는 truffle에서 인식하는 숫자입니다. migrate 명령 수행시 이전에 수행한 스크립트가 있으면 그 숫자 이후의 마이그레이션만 수행합니다. 만약 컨트랙 코드를 바꾸어 다시 배포해야 한다면 `truffle migrate --reset`과 같이 --reset 플래그를 주어 첫번째 스크립트부터 다시 실행하도록 할 수 있습니다.




참고로 `truffle migrate —network development`와 같은 방식으로 network를 지정할 수 있습니다.

잠시 migrations/2_deploy_contracts.js 를 살펴보겠습니다.



처음 부분의 artifacts.require(“…”) 구문은 해당 컨트랙의 artifacts를 불러오는 구문입니다. artifacts 객체는 truffle이 이 스크립트를 실행할 때 제공하는 객체이고, 컨트랙을 배포할 때 바이트코드를 얻어오거나 배포된 컨트랙과 상호작용할 때 사용됩니다. build/ 디렉토리 밑 json 파일에 컨트랙의 배포된 주소, ABI 등을 저장하기 때문에 가능합니다. 따라서 배포될 때 트랜잭션 결과로 나온 컨트랙 주소를 개발자가 직접 기록해둘 필요가 없습니다.

export 하는 함수는 ConvertLib과 MetaCoin을 배포합니다. MetaCoin이 ConvertLib을 라이브러리로 사용하기 때문에 중간에 deployer.link를 부르는 부분이 존재합니다.

truffle test


`truffle test` 명령어로 test 디렉토리 아래에 있는 테스트를 수행할 수 있습니다.




test는 Solidity와 Node.js 둘 다 작성할 수 있습니다. 이는 상황에 맞게 사용하면 됩니다. 테스트하려는 데이터의 타입이 자바스크립트보다는 Solidity가 더 적합한 경우도 있고, 케이스가 복잡하여 Solidity로는  작성하기 어려운 경우도 있습니다.

`truffle test` 명령어로 수행 가능합니다. `migrate`와 마찬가지로 `—network` 옵션으로 어떤 네트워크에서 테스트할 것인지 지정할 수 있습니다. 참고로 이더리움의 snapshot 기능을 사용하기 때문에 테스트 환경에서 발생한 트랜잭션이 누적되어 저장되지는 않습니다.

truffle exec

truffle 환경의 스크립트는 컨트랙 정보를 미리 저장해두기 때문에 때때로 유용합니다. 그러나 migrations/test에 들어가는 스크립트는 그 용도가 정해져 있습니다. 이 용도가 아닌 스크립트를 실행하기 위해서 truffle exec 명령어가 있습니다. 예를 들어 실배포 환경에서 컨트랙에 특정 함수를 호출하고 싶다면 아래와 같이 스크립트를 작성해서 `truffle exec <script_filename> --network <network>` 명령어를 실행하면 됩니다.

const MetaCoin = artifacts.require("MetaCoin");
 
module.exports = async function(deployer) {
        const instance = await MetaCoin.deployed();
        const { accounts } = web3.eth;

        const receipt = await instance.sendCoin(accounts[1], web3.toBigNumber("10"), { from: accounts[0] });
        console.log(receipt);
}


이처럼 Truffle을 사용하면 web3에 ABI와 컨트랙 주소를 넘겨줘서 컨트랙 객체를 얻어오는 과정을 간소화할 수 있습니다. 컨트랙 함수를 호출할 때 함수가 view 함수이면 receipt 대신 함수가 리턴하는 객체를 반환합니다. 컨트랙의 상태를 확인할 때에도 truffle 환경 스크립트를 사용하면 유용할 수 있습니다.


참고자료

Truffle: http://truffleframework.com/

web3.js 0.20.x API: https://github.com/ethereum/wiki/wiki/JavaScript-API

web3.js 1.0.x API: https://web3js.readthedocs.io/en/1.0/

Solidity: http://solidity.readthedocs.io/en/v0.4.21/index.html

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

이더리움에서는 랜덤 함수가 없습니다. 어떤 노드가 실행하더라도 같은 결과가 나와야만 트랜잭션을 검증할 수 있는 구조이기 때문입니다. 이는 확률 요소가 있는 게임이나 도박 응용을 구현하는데 문제가 됩니다. 트랜잭션을 미리 실행시켜보고 그 결과가 자신에게 불리할 경우 네트워크에 전파하지 않을 것이기 때문입니다. 이번 포스팅에서는 어떻게 랜덤 함수를 구현할 수 있는지에 대해 알아보겠습니다.



블록 스페셜 변수

우선 Solidity를 기준으로 랜덤 시드가 될 수 있는 스페셜한 블록 변수들이 있습니다.


- 블록의 번호

- 블록의 시간

- 블록해시


그러나 기본적으로 블록 변수들은 마이너에 의제 통제됩니다. 만약 트랜잭션의 결과가 블록 보상 이상의 인센티브가 있는 경우 이를 랜덤 시드로 쓰는 것은 적절하지 않습니다. 예를 들어 블록 보상이 3 이더인데, 100 이더짜리 도박판이 열리면 마이너는 PoW에 성공했더라도 그 블록에 자신이 도박판에서 지는 트랜잭션이 포함될 경우 그 블록을 발표하지 않고 버릴 수도 있음을 염두에 두어야 합니다.



블록의 번호를 쓸 경우 마이너 뿐만 아니라 유저에게도 예측될 수 있습니다. 예를 들어 1~6의 주사위를 던져 짝수가 나오면 이기는 게임이라 했을 때, 블록의 번호를 하나씩 올려가보면서 시뮬레이션하여 짝수가 나오는 블록에 포함되도록 트랜잭션 발표를 조정할 수 있습니다.


블록의 시간은 마이너에 의해 통제된다고 볼 수 있습니다.


블록의 해시는 마이너와 사용자 모두 예측하기 어렵지만, 마이너가 이해관계에 놓여있는 경우 위에서 언급한 것처럼 블록 자체를 발표 안 할 가능성이 존재합니다.



유저가 제출한 랜덤 시드를 XOR

블록 변수를 쓰는 것은 마이너가 이해당사자일 경우 동작을 안하는 단점이 있기 때문에, 이를 피하기 위해 유저가 직접 랜덤 시드를 제공하는 방법이 있습니다.


A, B가 1~6 주사위를 굴려서 홀짝을 가리는 게임을 한다고 가정해봅시다.


1. A는 랜덤한 값 x, B는 랜덤한 값 y를 생성해 자신만 알 수 있도록 기록해 둡니다. 생성한 랜덤 값 원본을 pre-image라 부릅니다

2. A, B는 pre-image를 해시한 값을 드러내는 트랜잭션을 합니다. 네트워크 상에선 hash(x), hash(y)가 발표되고 서로의 pre-image는 모르는 상태가 됩니다.

3. A, B 모두 해시값을 발표했으면 pre-image를 드러내는 트랜잭션을 합니다. A, B는 pre-image와 그 해시값을 검증한 뒤 (x XOR y) 값을 랜덤 시드로 쓰일 수 있습니다.


이는 A, B, C, D, ...로 확장되어도 똑같은 구성으로 랜덤 시드를 생성할 수 있습니다. 랜덤 시드를 제공한 사람들 중 일부가 담합하더라도, 그 중에서 단 한 명만이라도 랜덤한 값을 생성하였다면 결과 값이 랜덤해지는 속성이 있습니다.




유저가 제출한 랜덤 시드를 사용할 때 두 가지 주의점이 있습니다.

1. Replay

- A, B, C가 주사위 게임을 하고 B, C가 담합한 상태라고 가정합니다.

- B는 A를 따라 합니다.

  - A가 해시를 발표하면 B는 pre-image는 모르지만 해시를 똑같이 발표할 수는 있습니다.

  - pre-image를 발표하는 단계에서 B는 A가 발표하는 pre-image를 본 뒤에 똑같이 발표합니다.

- A와 B의 pre-image는 똑같으므로 XOR 연산에 의해 0이 되어서 C의 pre-image가 랜덤 시드가 됩니다.


해시를 만들때 [자신의 주소(공개키), pre-image]를 해시하도록 하면 B가 따라하더라도 검증 단계에서 걸러지게 됩니다.


2. DoS

pre-image를 발표하는 단계에서 마지막으로 발표를 하는 사람은 그 결과를 예측하고 자신의 pre-image를 발표하지 않을 수도 있습니다. 이를 위해 처음에 해시를 발표할 때 Timelock을 이용해서 일정 시간 안에 pre-image를 발표하지 않으면 패널티를 주는 장치를 해두어야 합니다.






참고:


http://solidity.readthedocs.io/en/develop/units-and-global-variables.html#special-variables-and-functions


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

이번 포스팅에선 이더리움 기반 토큰을 만들기 위한 Solidity 문법과 토큰 표준인 EIP20(ERC20)에 대해 알아보겠습니다.


첫 번째로 살펴볼 내용은 mapping 데이터 타입입니다.


예제 출처: http://solidity.readthedocs.io/en/develop/types.html#mappings


mapping은 일종의 key-value 스토리지 타입입니다. 기존 범용 프로그래밍 언어에서 흔히 볼 수 있는 Map<Key, Value>와 다른 점은 모든 키에 대해 Value가 존재하는 것처럼 동작합니다. 즉, 특정 키에 값을 쓰기 전에도 읽기가 가능하고, mapping 생성 시 초기화는 모든 키에 대해 값이 0인 것과 같습니다. 이런 특성 때문에 mapping 타입 자체에는 key에 대한 순회 방법이 존재하지 않습니다. key를 순회하기 위해서는 순회하려는 key의 리스트를 별도로 관리해야 합니다.


msg.sender는 transfer()를 호출한 이더리움 계정의 주소입니다. 즉, 위의 예제는 해당 이더리움 계정을 가진 사람만이 자신의 balance를 업데이트 할 수 있습니다.


이 mapping(address => uint) 타입이 이더리움 기반 토큰의 핵심을 담고 있습니다. 우리가 비트코인이나 이더리움의 이더를 생각해보면 어떤 암호화폐의 소유란 결국 내가 소유한 주소(공개키)에 얼마가 있는지가 공개되어 있다는 것입니다. 만약 우리가 이더리움 주소를 생성하면 그 주소에는 0 ETH가 들어있는 것으로 취급됩니다. 누군가 입금을 하기 전까지는요. mapping와 같지요. 이제 토큰 보내는 함수를 보겠습니다.



require(bool condition)는 조건이 만족하지 않으면 해당 트랜잭션을 취소합니다. 예제 구현에서는 자신이 가진 토큰보다 더 많은 토큰을 보내려 할 때 이를 막기 위해 쓰여졌습니다. 내 이더리움 계정을 가지고 transfer(address to, uint value) 함수를 호출함으로써 to 주소로 value 만큼 토큰을 보낼 수 있습니다. to는 다른 이더리움 계정이겠지요. 이러한 원리로 이더리움 계정은 ETH를 소유함과 동시에 이더리움 기반 토큰을 소유할 수 있습니다. 이제 남은 것은 토큰의 발행 정도이군요. 아주 간단한 예제를 하나 보겠습니다.



InitialSupplyExample은 생성자를 두어 생성자를 호출한 사람에게 10만 토큰을 넣습니다. 즉, 토큰을 만든 사람에게 10만 토큰을 발행한 것이지요. 위 예제의 경우 해당 토큰은 10만 개가 총 발행량이 되고, 더 이상 발행될 수 없습니다. 추후에 더 발행할 수 있는 토큰을 만들고 싶다면 아래처럼 작성해야 합니다.




mint 함수는 owner가 토큰을 새로 생성할 수 있는 함수입니다. 생성자를 호출한 사람을 owner로 기억해두기 때문에 해당 토큰을 만든 사람만이 토큰을 생성할 권리를 가지게 됩니다. 물론 위의 예제처럼 아무런 제약없이 토큰을 생성할 수 있다면 아무도 이 토큰에 가치를 신뢰하지 않을 겁니다.


토큰을 owner가 찍어내는 방식이 아닌 payable 함수를 두어 ETH를 받고 토큰을 발행해주는 방식도 생각해볼 수 있겠습니다. 스마트 컨트랙트 코드를 어떻게 짜느냐가 그 토큰의 특성을 정의하게 됩니다.


여기까지 간단한 이더리움 기반 토큰 구현에 대해 살펴보았고, 이제 ERC20 인터페이스에 대해 살펴보겠습니다. ERC20 토큰 표준안은 각종 토큰을 구현하는 스마트 컨트랙트가 지켜야 할 인터페이스입니다. EIP20 표준으로 채택되어 이 인터페이스를 지킨다면 지갑 앱이나 explorer 앱에서 이 토큰과 관련된 정보를 보여줍니다. 아래 스크린샷은 etherscan.io에서 Bread TOKEN 정보를 보여주는 예입니다.



아래는 EIP20 토큰이 지켜야 할 contract 인터페이스입니다.




여기서 Approval과 관련된 transferFrom/approve/allowance 함수는 설명이 필요할 것 같습니다. 이더리움 토큰은 토큰을 직접 보내는 방법도 있지만, 다른 사람이 내가 가진 토큰 사용을 허가하는 방법도 있습니다.


A라는 이더리움 계정이 approve(B, 100)을 호출하면 B 계정이 A 계정에 있는 토큰 100개를 사용할 수 있습니다. B는 transferFrom(A, C, 60)을 호출해서 C 계정에 60 토큰을 보낼 수 있습니다.

allowance는 balanceOf의 approval 버전입니다. A가 B에게 100을 approve하고, B가 C에게 transferFrom으로 60을 보내고 나면 allowance(A, B)는 40이 반환됩니다.


직접 transfer대신 approve하는 방식은 수수료 문제로 자주 쓰입니다. 예를 들어 어떤 토큰이 transfer 함수 내에서 msg.sender에게 1토큰의 수수료를 부과한다고 가정해 봅시다.


transfer 방식

A calls transfer(B, 100)

이 경우 A에서 101 토큰이 출금되고 B에 100이 입금됩니다.


approve 방식

A calls approve(B, 100)

B calls transferFrom(A, B, 100)

이 경우 A에서 100 토큰이 출금되고 B에 99가 입금됩니다.


수수료를 내는 주체가 달라집니다. 물론 이는 transfer에 수수료가 있다는 가정 하에 그렇습니다. 앞서 토큰 발행 시에 payable 함수를 두어 ETH를 토큰으로 바꾸는 예를 살짝 소개했는데, 토큰을 ETH로 바꾸는 환불의 경우 트랜잭션 수수료가 강제되기 때문에 ETH 출금같은 경우에는 거의 이 방식이 쓰입니다. 물론 이는 보내는 사람의 수수료 비용을 줄여주기도 하지만 ETH를 보낼 때 발생할 수 있는 보안 위협을 줄일 수 있는 방법이기도 합니다. 자세한 내용은 다른 보안 이슈 관련 포스팅에서 소개할 기회가 있으면 좋겠습니다.


지금까지 이더리움 기반의 토큰과 그 표준인 EIP20에 대해 알아보았습니다. 토큰에 관한 구체적인 구현이 궁금하신 분들은 OpenZeppelin의 라이브러리를 살펴보시면 도움이 될 것 같습니다.


https://github.com/OpenZeppelin/zeppelin-solidity/tree/master/contracts/token


20분만에 이더리움 토큰 발행하기라는 재미있는 포스팅도 있어 소개합니다. 이미 짜여진 라이브러리를 수정하면 이름 붙이고 어떻게 발행할 지만 결정하면 되는 정도라 매우 간단합니다.


https://medium.com/bitfwd/how-to-issue-your-own-token-on-ethereum-in-less-than-20-minutes-ac1f8f022793


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
지난 포스트에선 이더리움 스마트 컨트랙트의 출력 수단 중 하나인 이벤트에 대해 알아 보았습니다. 호출된 이벤트는 로그라는 이름으로 Transaction Receipt에 남아 이더리움 블록체인에 기록됩니다. 블록체인에 기록된 것은 블록을 다운로드 받을 수만 있다면 항상 접근할 수 있기 때문에 일종의 스토리지라고 생각할 수도 있습니다. 그러나 이 스토리지는 무한정 쓸 수는 없습니다. Gas라고 불리는 것에 의해 쓰는 만큼 비용을 내야 하고, 트랜잭션 당 쓸 수 있는 양에 제한이 있기 때문입니다.


Gas란?
Gas(이하 가스)는 이더리움이라는 거대한 컴퓨터를 쓰기 위한 일종의 연료입니다. 수수료는 가스 사용량과 가스 가격의 곱으로 결정됩니다. 이전 포스트에서 Solidity가 EVM이라는 스택머신의 바이트코드로 컴파일되고, 이더리움은 그 바이트코드를 실행한다고 언급했었습니다. EVM Opcode 마다 정해진 가스 사용량이 있습니다. 예를 들면 ADD 연산에 3가스, 곱하기 연산에 5가스, 트랜잭션에 21000가스, log 출력 시 바이트당 8가스 등등 입니다. 트랜잭션에서 발생한 모든 Opcode의 가스 사용량을 합하고 사용한 양에 1가스에 지불할 이더를 곱하면 해당 트랜잭션의 수수료가 계산됩니다.

트랜잭션 수수료 = 가스 사용량 * 가스 가격



Opcode에 따른 가스 사용량은 버전에 따라 조금씩 다르지만 중요한 것은 블록에 무언가 기록하는 Opcode가 비싸다는 것입니다. 블록에 기록된 내용은 이더리움의 모든 풀 노드가 저장해야 하기 때문입니다. 따라서 Solidity를 EVM 바이트코드로 변환할 때 중요한 것은 실행 속도가 아닙니다. 실행이 느리게 되더라도 바이트코드 사이즈를 줄여서 배포 시 발생하는 수수료를 줄이고, 가스 비용이 비싼 특정 Opcode들을 최소화하는 것이 좋겠지요.


Gas 가격의 결정과 지불
Gas 가격은 트랜잭션을 생성하는 사람이 1가스 당 지불할 wei를 결정합니다(wei는 현재 이더의 최소 단위입니다). 마이너가 트랜잭션을 처리하고 블록을 마이닝하면 그 블록에 수수료 지불내역이 남습니다. 

현재 가스의 적정 시세는 1 가스 당 40 Gwei(= 40000000000 wei = 0.00000004 ETH) 입니다. 트랜잭션에 21000 가스가 발생하니 기본 트랜잭션에 0.00084 ETH가 필요합니다. 1 ETH를 100만원으로 계산하면 기본 트랜잭션에 840원이 수수료로 발생하는 셈입니다. 송금앱들이 500원 수수료를 요구하는 것에 비해 더 비싸지만 비트코인 트랜잭션 수수료에 비하면 훨씬 합리적이네요.

가스 시세를 보여주는 ethgasstation 사이트에 따르면 1 가스당 40 Gwei를 지불하면 블록에 포함되는 시간이 1분 이내인 반면에 38 Gwei를 지불하면 약 30분 정도 걸릴 수 있다고 합니다.
https://ethgasstation.info/

트랜잭션을 생성할 때 가스 가격(GasPrice)과 같이 가스 제한(GasLimit)을 정해두어야 합니다. EVM은 튜링머신이기 때문에 실행해보기 전까지는 트랜잭션 처리가 종료될 지 알 수 없습니다. 가스 제한이 없다면 무한 루프 등으로 처리가 끝나지 않는 트랜잭션을 발생시켜 마이너들에게 DoS 공격을 가할 수 있습니다. 만약 트랜잭션이 제한된 가스 사용량을 넘기게 되면 그 트랜잭션은 실패된 채로 수수료가 지불됩니다. 가스 제한은 트랜잭션 뿐만 아니라 블록에도 존재하기 때문에 누군가 수수료를 많이 낼 수 있다고 해서 가스를 마음껏 쓸 수 있는 것은 아닙니다.

비트코인의 스크립트 언어가 튜링 완전하지 않기 때문에 종료가 보장되어 이러한 Gas 개념이 필요하지 않습니다.


Solidity 프로그래밍 내 Gas 제어 이슈
스마트 컨트랙트 프로그래밍에선 이 Gas 개념 때문에 고려해야 할 점들이 있습니다.

Solidity 프로그래밍 (1) 포스트에서 언급한 것과 같이 이더를 보낼 때 다른 Contract의 함수를 호출하게 되는 경우도 있습니다. 이더를 보낼 때 call을 호출해서 현재 내가 쓰고 남은 가스를 보낼 수 있는 방식과 send 혹은 transfer를 호출해서 남은 가스를 보내지 않는 방식을 구분해서 써야 합니다. 외부의 임의의 Contract가 악의적인 코드일 가능성을 염두에 두어 Gas를 필요한 만큼만 주는 것이 바람직합니다. 아래 링크는 Solidity 공식 문서의 re-entrancy 예제입니다.


6번 라인의 msg.sender.call.value(x)()는 x만큼 msg.sender에게 이더를 보내고 남은 가스를 넘겨줍니다. 만약 msg.sender가 악의적으로 작성된 스마트 컨트랙트일 경우 withdraw()를 다시 호출하는 re-entracy 공격을 할 수 있는데, 이 경우 코드에 따르면 msg.sender에게 계속 이더를 보내게 되고, 가스가 부족해지거나 Fund에 잔고가 다 떨어졌다고 판단되면 더 이상 withdraw()를 호출하지 않고 정상 종료하여 트랜잭션을 성공시킬 수 있습니다. 360만 이더가 탈취된 DAO 해킹이 이 취약점이 공격된 사례입니다.



두 번째는 Solidity 공식 문서의 Ballot 예제를 일부 따왔습니다. 코드 전체를 이해할 필요는 없고, 동적 길이의 배열과 그 배열 크기만큼 반복하는 for문에 대한 얘기입니다. winningProposal()이 view function 이여서 적합한 예제는 아니지만, 어느 정도 상상력을 가지고 보시면 되겠습니다. 누군가 ProposalNames를 길게 하여 여러번 Ballot을 호출했다고 가정해봅시다. 그 다음 winningProposal()을 호출하면 상당히 긴 for문을 돌아야 할 지도 모릅니다. 이 때 문제는 proposals의 길이를 줄일 수 있는 코드가 없고, 이미 길어진 proposals를 처리하는데 들어가는 가스가 블록 가스 제한을 넘어버리면, winningProposal()은 더 이상 호출할 수 없는 코드가 되어버립니다. DoS 공격이 가능해지는 것이지요.





이더리움의 가스와 관련된 내용에 대해 알아봤습니다. EVM이 가진 튜링완전 특성으로 인해 불가피하게 도입되었을텐데, 스마트 컨트랙트 프로그래밍 입장에선 상당히 저수준의 내용을 염두에 두고 있어야 해서 불편한 부분이기도 합니다. 다음 포스팅에서는 MetaCoin이라는 예제를 기반으로 이더리움 기반의 코인이란 어떤 것인지 알아보도록 하겠습니다.


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.


지난 포스트에서는 이더리움이 추상적으로 어떻게 동작하는지와 컨트랙트의 생성, 함수 호출에 대해 알아보았습니다. 이번 포스팅에서는 이벤트의 개념과 사용, 블룸 필터 자료구조를 소개하겠습니다. P2P 네트워크 상에는 나와는 무관한 트랜잭션들이 떠돌아(?) 다니기 때문에 내가 관심있는 트랜잭션만 효과적으로 식별하기 위해 블룸 필터를 사용하고, 비트코인에서도 마찬가지의 이유로 사용됩니다.


이벤트

이전 예제에서는 이더를 주고 받을 수 있는 컨트랙트 예제를 설명드렸습니다. 프로그래밍을 하는 관점에서 봤을 때, 스마트 컨트랙트의 출력(output)은 이더입니다. 그러나 이더를 보내는 것은 제한된 용도의 출력입니다. 오늘 설명드릴 내용은 Event라고 하여 스마트 컨트랙트의 다른 출력 수단입니다.


Event(이하 이벤트)란 트랜잭션 내에서 호출될 수 있는 일종의 리턴값이 없는 함수입니다. 이벤트를 호출하면 그 호출한 기록이 Transaction Receipt라 불리는 트랜잭션 결과에 저장됩니다. 일종의 로그(log)입니다. 기술적인 상세는 뒤에서 설명하고 우선 코드 예제를 보겠습니다.



이더를 받는 폴백 함수에서 누가 얼마를 보냈는지를 기록하는 컨트랙트입니다. 지난 시간에 소개한 것과 같이 위와 같이 한 번 배포된 컨트랙트는 내용이 바뀔 수 없습니다. 위의 컨트랙트에 돈을 보내면 태웠다는 로그만 남으며 보낸 이더는 꺼낼 수 있는 방법이 없습니다. 돈이 태워진 것입니다.


위의 로그가 발생했다는 사실은 아래처럼 읽을 수 있습니다. 아래는 web3.js 라이브러리를 사용한 자바스크립트 예제입니다.




코드에 대해 부연설명을 하면 web3.js는 이더리움 노드의 RPC 함수들을 자바스크립트로 제어하기 위한 라이브러리입니다. abi 변수는 컨트랙트의 인터페이스 데이터입니다. 한 번 컴파일되어 배포된 코드와 그 입출력은 결국 ByteArray이기 때문에 그 데이터를 해석할 수 있는 스키마가 필요합니다. ABI에 대한 자세한 내용은 아래 링크에서 확인할 수 있습니다.


https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI


저 코드를 이용해서 누가 제일 많이 태웠는지 보여주는 웹 페이지를 만드는 등을 상상해볼 수 있겠죠. :) 그런데 이더리움 네트워크에는 하루에도 수만 개의 트랜잭션이 발생하는데, 이 트랜잭션에서 내가 원하는 이벤트가 발생했다는 사실을 어떻게 알고 콜백을 받을 수 있을까요? 모든 트랜잭션의 로그를 들여다보는 것은 매우 비효율적입니다.


Topic과 Bloom Filter

이벤트 호출을 Topic으로 변환하고 그 토픽을 블룸 필터로 만듭니다. 현재 프로토콜 상 토픽은 4개로 제한되고, 첫 번째 토픽은 이벤트 시그니처를 해시하여 만듭니다.


SHA-3("burned(address,uint)") = 0x0970ce1235167a71...


이렇게 생성된 토픽들을 블룸 필터로 만듭니다.




블룸 필터에 대해 위의 그림 예제로 간단히 설명하겠습니다. x를 삽입할 때 x를 서로 다른 3개의 해시 함수로 얻은 값을 포인터로 사용해 해당 비트를 1로 표시합니다(파란색 화살표). 이렇게 한 뒤 x의 삽입 여부를 알고 싶다면 x를 다시 해시하여 해당 비트가 모두 1인지 여부를 확인하면 됩니다. 물론 해시 충돌로 인해서 False Positive 응답이 나올 수 있습니다.


블룸 필터에 데이터 w가 있었는지 여부를 물으면 대답은 두 가지입니다.

- "w가 있을 수도 있다"

- "w는 없다"


이더리움 노드는 트랜잭션을 실행한 결과를 블룸 필터를 만들고, 수십 개의 트랜잭션을 모아서 블록으로 만들 때에도 각 트랜잭션의 블룸 필터를 모아 하나로 만듭니다. 새로운 블록이 만들어졌을 때 내가 관심있는 이벤트가 발생했는지 여부를 알고 싶으면 해당 이벤트의 Topic을 블록 헤더의 블룸 필터에 조회해보면 됩니다. 만약 없다면 해당 블록에 이벤트가 발생하지 않았음을 바로 알 수 있습니다.


앞서 토픽은 4개로 제한되어 있다고 했습니다. 이벤트 시그니처를 제외한 나머지 토픽은 Indexed Parameter로 지정할 수 있습니다. 위의 예제를 확장하여 다시 코드를 보여드리겠습니다.



파라메터 타입 뒤에 "indexed" 키워드가 추가되었습니다.



web3.js 코드에는 callback 인자 앞에 { amount: 10 } 을 넘겨주고 있습니다. 이는 amount 값이 10인 로그만 콜백으로 받겠다는 의미입니다. 마찬가지로 who 에도 indexed를 주었기 때문에 특정 어카운트가 돈을 태웠는지를 이벤트로 받을 수도 있습니다.


Solidity의 기본형들은 32바이트인 토픽 사이즈 내에 들어오기 때문에 해시를 하지 않고, 배열의 경우 해시값이 토픽으로 들어갑니다. 일반적으로 배열이 토픽이 되는 경우는 없지만, 배열이 토픽으로 들어갈 경우 트랜잭션 결과(Receipt)에 로그가 저장될 때 배열이 복사되는게 아닌 해시값이 들어갑니다. 이 경우 원본 데이터가 날아가는 것은 아닙니다. 해당 트랜잭션을 내 이더리움 노드가 실행하여 재생산할 수 있기 때문입니다.


Topic은 4개로 제한되어 있기 때문에 indexed는 3개까지만 지정할 수 있습니다. 예외적으로 이벤트 시그니처를 Topic으로 취급하지 않는 익명(anonymous) 이벤트는 indexed 파라메터를 4개까지 가질 수 있습니다.


정리

지금까지 Solidity의 이벤트에 대해 다소 자세히 살펴봤습니다. 이더리움 네트워크는 스마트 컨트랙트를 실행하기 위한 단 한 대의 컴퓨터라고 생각할 수 있습니다. 그 컴퓨터의 출력은 이더를 송금하거나, 이벤트를 발생시키는 것입니다. 단 한 대만 존재하는 컴퓨터이기 때문에 이벤트를 무한히 발생시켜서는 안되기 때문에 트랜잭션 실행 비용을 받습니다. 다음 포스팅에선 Gas에 대해 알아보도록 하겠습니다.





336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

이더리움 스마트 프로그래밍 언어인 Solidity 언어 예제를 기반으로 이더리움 스마트 컨트랙트의 생성과 함수 호출에 대해 설명하는 글입니다.


Solidity 프로그래밍에 앞서 Solidity의 런타임 환경인 이더리움이 추상적으로 무엇을 하고, 어떻게 프로그램이 동작하는지 이해할 필요가 있습니다. 우선 블록체인 기술을 P2P 분산 스토리지로 생각할 수 있습니다. 블록체인 자체에 대한 내용은 다른 포스팅으로 소개하겠습니다. 이더리움은 비트코인과 마찬가지로 어떤 주소로 이더(Ether, ETH)를 주고 받을 수 있고, 주고 받은 내역이 스토리지에 됩니다. 이더리움의 차별점은 거래내역만 저장할 뿐 아니라 이더를 입출금할 수 있는 코드와 그 실행 결과를 저장합니다. 즉, 이더리움 네트워크에서 이더를 주고 받을 수 있는 주소는 2가지 종류가 있습니다. 이더리움에서는 주소라는 말 대신 어카운트(Account)라는 말을 사용하기에 아래부턴 어카운트라고 지칭하겠습니다.


- 이더리움의 어카운트 종류


이더리움에는 두 가지 종류의 어카운트가 있습니다. 하나는 Externally Owned Account, 줄여서 EOA라고도 부르며, 공개키/비밀키 쌍을 만들고 공개키를 공개하는 원리를 생각하면 되며 만들기 쉽습니다. 이 공개키로 만든 주소를 이더를 보내줄 사람에게 알려주면 이더를 받을 수 있고, 받은 이더를 다른 사람에게 보내려면 비밀키로 서명하여 네트워크에 전파하면 됩니다. 서명된 거래내역(트랜잭션)이 어떻게 다른 노드들에 의해 받아들여지고 위변조가 불가능해지는지는 비트코인 화이트페이퍼(https://bitcoin.org/bitcoin.pdf)를 참고하는 것을 추천합니다. 디테일로 들어가면 이더리움과 비트코인은 다르지만 키 아이디어를 이해하는데에는 효과적입니다. 다른 어카운트 종류 하나는 Contract 입니다. 저번 포스트에 이어 코드를 우선 보겠습니다.


위의 Contract는 bet()을 호출할 때 받은 돈을 prize_money로 누적시켜 기억해뒀다가 10번째 호출하는 사람에게 몰아주는 복권입니다. 실질적으로는 누구나 count 값을 알 수 있기 때문에 실제로 쓰이긴 어려운 예제입니다. 저번 예제과 다르게 payable이나 msg.value, msg.sender.transfer 등이 나타나면서 설명이 필요할 것 같습니다. payable은 이 함수를 호출할 때 이더를 보낼 수 있다는 것을 의미합니다. payable이 아닌 경우 이 함수를 호출할 때 이더를 보낼 수 없습니다. msg는 특수한 객체인데, 이 함수가 호출된 컨텍스트 정보를 얻어올 수 있습니다. msg.value는 전달된 이더 값이고, msg.sender는 이더를 보낸 어카운트의 주소입니다. Solidity에서는 주소 타입이 있어 balance()나 transfer() 등을 통해 해당 어카운트의 잔고를 확인하거나 이더를 보내는 함수를 호출할 수 있습니다.


사실, 위의 예제를 더 정확히 이해하기 위해서는 실행 환경에 대해 더 이해할 필요가 있어 그 얘기를 하려 합니다.


- Contract 인스턴스의 특정 함수를 실행시키는 방법

- Contract 인스턴스를 생성하는 방법


이더리움의 Contract 객체 생성이나 함수 호출은 트랜잭션에 의해 이루어집니다. 트랜잭션은 아래와 같은 정보를 포함합니다.


- 보내는 사람의 주소

- 받는 사람의 주소

- 이더

- 입력 데이터

- 가스 제한

- 가스 가격


가스에 관련된 내용은 이어질 포스팅에서 다루도록 하겠습니다. 보내는 주소/받는 주소/이더 외에 데이터 항목이 있다는 것에 주목하시면 됩니다. 함수를 호출하기 위해선 함수를 지정하는 Function Selector와 함수에 넘길 인자를 인코딩하여 전달해야 합니다. 기술적인 상세한 내용은 ABI 스펙 문서(https://solidity.readthedocs.io/en/develop/abi-spec.html)를 보면 됩니다. 만약 유효한 Function Selector가 아닌 경우 Contract에 정의되어 있는 Fallback 함수가 호출됩니다. Fallback 함수가 없으면 트랜잭션은 실패합니다. 아래는 위의 예제를 bet() 함수 대신 Fallback 함수로 바꾼 것입니다. Fallback 함수는 해당 Contract 내의 이름이 없는 유일한 함수이며 함수 인자를 받거나 값을 리턴하지 않습니다. Fallback 함수로 바꾸면 이 Contract에 그냥 이더를 보내는 트랜잭션으로 함수를 호출한 것과 같은 효과를 낼 수 있습니다.

스마트 컨트랙트의 보안에서 신경써야 할 점 중 하나는 msg.sender를 포함해서 어떤 address가 Contract 일 수 있다는 점입니다. msg.sender.transfer()를 호출하는 순간 단순히 내가 이더를 보내는 것이 아닌 다른 사람이 작성한 Fallback 함수가 실행될 수 있다는 점입니다.


다음은 Contract의 생성(혹은 deploy)인데, 이더리움은 Contract의 생성을 위한 프로토콜을 가지고 있습니다. 받는 사람의 주소를 0x0으로 주고 입력 데이터에 Solidity 코드를 컴파일한 EVM Bytecode를 넘겨주는 것입니다. Constructor에 전달해줘야 하는 인자가 있다면 역시 ABI 인코딩하여 넘겨줘야 합니다. 인스턴스의 파괴는 해당 인스턴스 내에서 selfdestruct 함수를 호출해야 합니다. 인스턴스의 주의해야 할 특징으로는 한 번 deploy 된 코드는 바꿀 수 없다는 점입니다. 코드를 업데이트 해야 하는 요구사항이 있을 경우 업데이트된 Contract의 인스턴스를 deploy 한 뒤 기존 인스턴스의 변수를 새 인스턴스로 이전하거나 Proxy 역할을 하는 Contract를 써야 하는 등의 문제가 있습니다. 만약 selfdestruct를 호출할 수 없는 Contract는 이더리움 프로토콜이 바뀌지 않는 한 영원히 파괴되지 않습니다. 


여기까지 이더를 주고 받을 수 있는 스마트 컨트랙트 예제와 컨트랙트의 생성과 함수 호출에 대해 살펴봤습니다. 설명을 위해 간단한 예제만 보여드렸지만 응용은 다양하게 있을 수 있습니다. 대표적인 예제로는 투표 시스템, 크라우드 펀딩, 경매 시스템 등이 있습니다. 이런 응용들을 블록체인으로 하면서 얻게 되는 장점으로는 암호학적으로 증명되는 투명성과 거래의 보장입니다. 크라우드 펀딩을 예로 설명하면 환불과 관련된 규정을 적고 고객을 설득 할 필요없이 코드로 작성하여 공개하면 됩니다. 단점으로는 한번 배포된 계약은 사업 주체의 통제에서 벗어나 해커의 공격 대상이 될 수 있다는 점입니다.

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
비트코인과 이더리움, 그리고 기타 블록체인에 기반한 코인들의 거래 가격이 폭등하면서 이젠 지나가는 할아버지도 비트코인 얘기하는 것을 어렵지 않게 볼 수 있는 것 같습니다. 이 포스트는 투자와 관련하여 쓰는 글은 아닙니다. 저는 개발자이고, 블록체인 기술 스타트업에 조인해서 일을 하고 있습니다. 제 친구들 역시 대부분이 개발자입니다. 최근에 제 친구들에게 제가 하는 일을 소개하기 위해 구구절절 설명하게 되는 일이 많아졌음을 느꼈고, 뭔가 정리된 상태로 전달할 수 있으면 좋을 것 같아서 개발자를 위해 간단한 소개 글을 쓰고 싶어졌습니다. 다른 관점에서 보면, 개발자들이 암호화폐에 가지는 관심에 비해 그 기반 기술에 대한 이해는 부족한 것 같습니다.

이를 위한 첫 시리즈는 Solidity 코드로 들여다보는 이더리움 스마트 컨트랙트 프로그래밍입니다. 앞서 간단히 말하자면, 블록체인을 일종의 분산 스토리지로 생각할 수 있고 이더리움은 그 스토리지에 코드와 그 실행 결과를 저장하는 시스템이라고 볼 수 있습니다. 이더리움의 코드 실행은 EVM이라고 불리는 스택 머신에 의해 실행되고, Solidity는 EVM 바이트코드로 컴파일되는 언어 중 하나입니다. 이 시리즈에선 여러분들에게 이더리움 클라이언트를 설치하거나 코드를 실행해보는 것을 요구하진 않습니다. 그저 눈으로 코드를 훑어보고 그 특성을 이해하는 정도로 넘어갈 것이며 따라서 엄밀한 예제를 제공하진 않을 것입니다.


위는 간단한 Solidity 코드입니다. 기존 class 기반의 OOP 프로그래밍 경험이 있다면, contract 대신 class를 대입해보면 간단히 이해할 수 있을 것이라 생각합니다. 이어지는 포스팅에서는 위의 코드처럼 다른 프로그래밍 언어와 비슷한 부분은 설명하지 않을 것입니다. 그런 부분은 지루하고, 또 공식 문서나 다른 글에서 이미 잘 설명하고 있기 때문입니다. 따라서 위의 코드를 보고 어떻게 동작하는지 짐작가는 바가 없는 분에게는 이해가 어려울 수도 있습니다.


다음 포스트




+ Recent posts