저는 이번주에 스마트 컨트랙트 개발자들이 컨트랙트를 작성시 이용하게될 API 개발에 집중하였습니다. API 디자인의 이해를 돕기위해 스마트 컨트랙트를 작성하는 예시를 직접 보여드리겠습니다. 이번에 소개드리는 예시는 단순 화폐의 교환 과정보다 다소 복잡하나 EOS와 다른 화폐간의 완벽한 거래 과정을 보여줍니다.
EOS 기반의 개발자는 C로 스마트 컨트랙트를 작성한 후 웹 어셈블리로 컴파일 하고, 블록 체인에 배포합니다. 이는 정적 타입인 C의 템플릿 시스템을 활용해 계약의 안정성을 보장할 수 있다는 것을 의미합니다.
차원 분석은 가장 기본적인 안정성의 한 종류로 알려져있습니다.(단위의 정확성 유지를 의미합니다) 교환을 위한 코드를 작성할 때 EOS, 타 화폐, 타 화폐 1개 당 EOS의 수(EOS/CURRENCY) 등 화폐 간 서로 다른 단위를 고려해야합니다.
구현 예시는 다음과 같습니다.
struct account {
uint64_t eos_balance;
uint64_t currency_balance;
};
이 간단한 접근 방식이 갖는 문제점은 실수로 다음과 같은 코드가 작성될 수 있다는 것입니다.
void buy( Bid order ) {
...
buyer_account.currency _balance -= order.quantity;
...
}
언뜻보기에 오류가 드러나지 않지만, 자세히 들여다보면 타 화폐가 아닌 EOS로 구매하고 있다는 것을 알 수 있습니다. 이 경우 시장 가격은 EOS를 기준으로 책정됩니다. 발생할 수 있는 또 다른 오류는 다음과 같습니다.
auto receive_tokens = order.quantity * order.price;
위 코드는 매수일 때 유효하나 매도일 경우 가격 변수를 음수로 표현해야 합니다. 정확한 차원 분석 없이는 사과에 사과를 추가했는 지, 오렌지에 사과를 추가했는 지 분간할 방법이 없습니다.
다행히 C++의 템플릿과 연산자 오버로딩을 이용하면 런타임 비용 없이 단위 검증을 수행할 수 있습니다.
template<typename NumberType, uint64_t CurrencyType = N(eos) >
struct token {
token(){}
explicit token( NumberType v ):quantity(v){};
NumberType quantity = 0;
token& operator-=( const token& a ) {
assert( quantity >= a.quantity,
"integer underflow subtracting token balance" );
quantity -= a.quantity;
return *this;
}
token& operator+=( const token& a ) {
assert( quantity + a.quantity >= a.quantity,
"integer overflow adding token balance" );
quantity += a.quantity;
return *this;
}
inline friend token operator+( const token& a, const token& b ) {
token result = a;
result += b;
return result;
}
inline friend token operator-( const token& a, const token& b ) {
token result = a;
result -= b;
return result;
}
explicit operator bool()const { return quantity != 0; }
};
이같은 정의를 사용함으로써 계좌 내 명확한 타입 구분이 생겼습니다.
struct Account {
eos::Tokens eos_balance;
currency::Tokens currency_balance;
};
struct Bid {
eos::Tokens quantity;
};
아래와 같이 코딩하면 -=연산자에 대해 eos::Tokens 타입인지 currency::Tokens 타입이 정의되지 않았기 때문에 컴파일 오류가 발생하게 됩니다.
void buy( Bid order ) {
...
buyer_account.currency _balance -= order.quantity;
...
}
저는 이 기술을 이용해 교환 컨트랙트 예제에서 나타난 단위 불일치 문제들을 확인하고 고치는 컴파일 작업을 수행할 수 있었습니다. 이 기술의 가장 큰 장점은 C++ 컴파일 결과로 최종 생성된 웹 어셈블리가 unit64_t를 이용한 잔액의 결과와 완벽히 동일하다는 것입니다.
또 한 가지 주목할 점은 token 클래스가 자동으로 오버 플로우와 언더 플로우에 대한 예외 처리를 한다는 것입니다.
저는 컨트렉트 교환을 체결하면서 타 화폐의 컨트랙트를 먼저 변경했습니다. 교환 컨트랙트가 타 화폐 컨트랙트에 의해 정의 된 타입에 접근할 수 있도록 해더 파일 currency.hpp와 소스 파일 currency.cpp을 리팩토링 해보았습니다.
#include <eoslib/eos.hpp>
#include <eoslib/token.hpp>
#include <eoslib/db.hpp>
/**
* Make it easy to change the account name the currency is deployed to.
*/
#ifndef TOKEN_NAME
#define TOKEN_NAME currency
#endif
namespace TOKEN_NAME {
typedef eos::token<uint64_t,N(currency)> Tokens;
/**
* Transfer requires that the sender and receiver be the first two
* accounts notified and that the sender has provided authorization.
*/
struct Transfer {
AccountName from;
AccountName to;
Tokens quantity;
};
struct Account {
Tokens balance;
bool isEmpty()const { return balance.quantity == 0; }
};
/**
* Accounts information for owner is stored:
*
* owner/TOKEN_NAME/account/account -> Account
*
* This API is made available for 3rd parties wanting read access to
* the users balance. If the account doesn't exist a default constructed
* account will be returned.
*/
inline Account getAccount( AccountName owner ) {
Account account;
/// scope, code, table, key, value
Db::get( owner, N(currency), N(account), N(account), account );
return account;
}
} /// namespace TOKEN_NAME
#include <currency/currency.hpp> /// defines transfer struct (abi)
namespace TOKEN_NAME {
/// When storing accounts, check for empty balance and remove account
void storeAccount( AccountName account, const Account& a ) {
if( a.isEmpty() ) {
printi(account);
/// scope table key
Db::remove( account, N(account), N(account) );
} else {
/// scope table key value
Db::store( account, N(account), N(account), a );
}
}
void apply_currency_transfer( const TOKEN_NAME::Transfer& transfer ) {
requireNotice( transfer.to, transfer.from );
requireAuth( transfer.from );
auto from = getAccount( transfer.from );
auto to = getAccount( transfer.to );
from.balance -= transfer.quantity; /// token subtraction has underflow assertion
to.balance += transfer.quantity; /// token addition has overflow assertion
storeAccount( transfer.from, from );
storeAccount( transfer.to, to );
}
} // namespace TOKEN_NAME
교환 컨트랙트는 송신자와 수신자가 거래할때마다 currency::Transfer와 eos::Transfer 사이의 메시지를 처리합니다. 이 컨트랙트는 구매, 판매, 취소 3가지 메시지를 처리합니다. 교환 컨트랙트는 메시지 타입과 데이터베이스 테이블이 정의되는 exchange.hpp의 공개 인터페이스를 정의합니다.
#include <currency/currency.hpp>
namespace exchange {
struct OrderID {
AccountName name = 0;
uint64_t number = 0;
};
typedef eos::price<eos::Tokens,currency::Tokens> Price;
struct Bid {
OrderID buyer;
Price price;
eos::Tokens quantity;
Time expiration;
};
struct Ask {
OrderID seller;
Price price;
currency::Tokens quantity;
Time expiration;
};
struct Account {
Account( AccountName o = AccountName() ):owner(o){}
AccountName owner;
eos::Tokens eos_balance;
currency::Tokens currency_balance;
uint32_t open_orders = 0;
bool isEmpty()const { return ! ( bool(eos_balance) | bool(currency_balance) | open_orders); }
};
Account getAccount( AccountName owner ) {
Account account(owner);
Db::get( N(exchange), N(exchange), N(account), owner, account );
return account;
}
TABLE2(Bids,exchange,exchange,bids,Bid,BidsById,OrderID,BidsByPrice,Price);
TABLE2(Asks,exchange,exchange,bids,Ask,AsksById,OrderID,AsksByPrice,Price);
struct BuyOrder : public Bid { uint8_t fill_or_kill = false; };
struct SellOrder : public Ask { uint8_t fill_or_kill = false; };
}
교환 컨트랙트의 소스 코드는 이 포스팅에 전부 게시하기에 다소 길지만, 깃허브를 방문하시면 전체 소스 코드를 보실 수 있습니다. SellOrder에 적용된 아이디어를 설명해 드리기 위해 여기에 이용된 코어 메시지 핸들러의 코드를 보여드리겠습니다.
void apply_exchange_sell( SellOrder order ) {
Ask& ask = order;
requireAuth( ask.seller.name );
assert( ask.quantity > currency::Tokens(0), "invalid quantity" );
assert( ask.expiration > now(), "order expired" );
static Ask existing_ask;
assert( AsksById::get( ask.seller, existing_ask ), "order with this id already exists" );
auto seller_account = getAccount( ask.seller.name );
seller_account.currency_balance -= ask.quantity;
static Bid highest_bid;
if( !BidsByPrice::back( highest_bid ) ) {
assert( !order.fill_or_kill, "order not completely filled" );
Asks::store( ask );
save( seller_account );
return;
}
auto buyer_account = getAccount( highest_bid.buyer.name );
while( highest_bid.price >= ask.price ) {
match( highest_bid, buyer_account, ask, seller_account );
if( highest_bid.quantity == eos::Tokens(0) ) {
save( seller_account );
save( buyer_account );
Bids::remove( highest_bid );
if( !BidsByPrice::back( highest_bid ) ) {
break;
}
buyer_account = getAccount( highest_bid.buyer.name );
} else {
break; // buyer's bid should be filled
}
}
save( seller_account );
if( ask.quantity ) {
assert( !order.fill_or_kill, "order not completely filled" );
Asks::store( ask );
}
}
위 코드는 상대적으로 간결해서 읽기 쉽고 안전하며 최상의 성능을 내고 있습니다.
프로그래밍 언어 논쟁에 참여해보신 분들은 C와 C++ 프로그래머의 메모리 관리 문제를 들어보셨을 것입니다. 다행히도 스마트 컨트랙트에서는 메시지가 들어올 때마다 ‘재시작’함으로써 슬레이트를 비우기 때문에 이 같은 문제가 사라집니다. 다행히도 스마트 컨트랙트에서는 동적 메모리 할당을 구현할 필요가 거의 없습니다. 전체 교환 컨트랙트에서 new, delete, malloc, free를 호출하지 않습니다. 웹어셈블리 프레임워크는 메모리 오류를 야기하는 트랙잭션을 자동으로 거부합니다.
즉 짧은 수명의 메시지 핸들러에서는 C++의 단점들이 사라지고 많은 이점들만 남게 됩니다.
EOS.IO 소프트웨어는 잘 진행되고 있으며 이 API를 이용해 스마트 컨트랙트를 작성하는 일은 정말 즐겁습니다.
원문: https://steemit.com/eos/@dan/eos-example-exchange-contract-and-benefits-of-c
@yguhan 님께서 번역해주신 글입니다.