📒 EOS - Пример биржевого контракта и преимущества C++ (@dan)

in eos •  7 years ago  (edited)

На этой неделе я сосредоточил своё внимание на API, который разработчики смарт-контрактов будут использовать для написания контрактов. Чтобы помочь упростить структуру этого API, я самостоятельно реализовал пример типового контракта. На этот раз пример немного сложнее, чем просто валютный контракт, но он представляет собой целый обменник между встроенной валютой EOS и гипотетическим контрактом CURRENCY.

Преимущества API на C ++

Разработчики, использующие в качестве основы EOS, будут писать свои смарт-контракты на C++, потом написанный на нем программный код будет компилироваться в Web Assembly, а затем публиковаться в блокчейн. Другими словами, мы можем использовать систему типов и шаблонов C ++ для обеспечения безопасности наших контрактов.

Одним из основополагающих моментов обеспечения безопасности является анализ размерности, главная идея которого состоит в четком отслеживании использующихся единиц измерения. При разработке биржи вы имеете дело с несколькими единицами измерения: EOS, CURRENCY и EOS / CURRENCY.

Простая реализация данного момента предполагала бы нечто подобное:

struct account {
  uint64_t  eos_balance;
  uint64_t  currency_balance;
};

Проблема использования данного простого подхода заключается в том, что случайно может быть записан следующий код:

void buy( Bid order ) {
  ...
  buyer_account.currency _balance  -=  order.quantity;
  ...
}

На первый взгляд ошибка неочевидна, но при более тщательной проверке можно заметить, что ордера на покупку (Bids) используют eos_balance, а не currency_balance. В этом случае рынок устанавливает цену CURRENCY в EOS.

Также может возникнуть следующая ошибка:

auto receive_tokens = order.quantity * order.price;

Эта конкретная строка кода может быть валидна, если ордер представляет собой предложение на покупку (Bid), но в случае, если это предложение на продажу (Ask), цена должна быть инвертирована. Как видите, без надлежащего анализа разрядности вы не можете быть уверены, что складываете яблоки с яблоками, а не яблоки с апельсинами.

К счастью, 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;
};

При использовании описанного выше кода будет генерироваться ошибка компиляции, потому что -= operator не определён для eos::Tokens и currency::Tokens.

void buy( Bid order ) {
   ...
   buyer_account.currency _balance  -=  order.quantity;
   ...
}

Применяя этот метод, я смог использовать компилятор для определения и исправления многих несоответствий типов единиц измерения в моей реализации примера биржевого контракта. Самое приятное заключается в том, что результирующий код веб асемблера, сгенерированный компилятором C++, идентичен тому, что было бы сгенерировано, если бы я просто использовал тип uint64_t для всех своих балансов.

Еще один момент, который вы можете отметить, заключается в том, что класс token также автоматически проверяет исключения переполнения и переполнения снизу.

Упрощенный валютный контракт

В процессе написания биржевого контракта я сначала обновил валютный контракт. При этом я переделал код валютного контракта в файл заголовка currency.hpp и исходник currency.cpp, чтобы биржевой контракт мог получить доступ к типам, определенным валютным контрактом.

currency.hpp

#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

currency.cpp

#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 всякий раз, когда биржа является отправителем или получателем. Он также реализует три собственных сообщения: купить, продать и отменить. Биржевой контракт определяет собственный открытый интерфейс, типы сообщений и таблицы баз данных в файле exchange.hpp.

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; };
}

Текст исходного программного кода биржевого контракта слишком объёмен для этого поста, но вы можете увидеть его на github. Чтобы вы получили представление о том, как он будет реализован, я представлю вам основной обработчик сообщений для 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 и C ++ при управлении памятью. К счастью, большинство этих проблем исчезают при создании смарт-контрактов, потому что ваша программа “перезагружается” и возобновляет работу “с чистого листа” в начале обработки каждого сообщения. Кроме того, необходимость реализовать динамическое распределение памяти возникает крайне редко. Биржевой контракт не вызывает ни функции new и delete, ни malloc и free. Фреймворк WebAssembly автоматически отклонит любую транзакцию, которая неправильно обращается к памяти.

Это означает, что большинство проблем C ++ исчезают при его использовании для обработчиков сообщений с коротким жизненным циклом, а взамен у нас остается множество преимуществ.

Заключение

Программное обеспечение EOS.IO прекрасно развивается, и я как никогда счастлив оттого, насколько проще писать смарт-контракты, используя этот API.


Свежие новости в Телеграм: t.me/EOS_RU


Переведено @rusteemitblog

Оригинал поста: ЗДЕСЬ


Поддержите witness blockchained на Steem


Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Спасибо !очень интересно ! удачи и процветания !

Блин ничего не понятно. Никогда не слышал о приемуществах C ++ кроме наличия функций new и malloc :-)

your post looks interesting
i wish there was an english translation
anyway thanks for sharing

хороший пост

Here you'll find only officially released television promos and teasers, along with exclusive interviews and more. Make sure to follow to never miss a video!