В этом уроке вы найдете несколько практических советов. Одна из самых больших проблем при разработке смарт-контрактов C # NEO связана с языковыми функциями, поддерживаемыми NeoVM, о которых на практике не говорит официальный документ. Также есть несколько полезных советов по взаимодействию с хранилищем и генерации случайности. Наслаждайтесь взломом.

Преобразование типов

Основным типом, поддерживаемым NeoVM, является массив байтов (Byte []), затем обычно используемые логические значения, String и BigInteger. Существуют и другие целочисленные типы, такие как Int8, Int64, UInt16, long, ulong и т. Д., Которые можно неявно преобразовать в BigInteger. Плавающий не поддерживается.

Итак, давайте сосредоточимся на преобразованиях между Byte [], Boolean, String и BigInteger. Примечание. Некоторые преобразования не определены официально, в данном случае я пытаюсь найти наиболее разумную реализацию.

Из байта [] в логическое

Хотя выглядит самым простым, на самом деле прямого преобразования нет. Официальная инструкция только упоминает, что False равно int 0. Итак, предположим, что True равно всем остальным значениям, в то время как пустой массив байтов также равен False. Итак, мы определяем следующую функцию:

public static bool BytesToBool(byte[] data) => data[0] != 0;

Тогда мы получим следующий результат:

bool b0 = Bytes2Bool(new byte[0]); //False
bool b1 = Bytes2Bool(new byte[1]{0}); //False
bool b2 = Bytes2Bool(new byte[1]{1}); //True
bool b3 = Bytes2Bool(new byte[2]{0,2}); //False
bool b4 = Bytes2Bool(new byte[3]{3,2,5}); //True

Байт [] в строку

Это напрямую предоставлено Neo.SmartContract.Framework.Helper

public static string BytesToByte(byte[] data) => data.AsString();

Byte [] в BigInteger

public static BigInteger BytesToBigInteger(byte[] data) => data.AsBigInteger();

Логическое в байтовое []

Это также требует ручного преобразования.

public static byte[] Bool2Bytes(bool val) => val? (new byte[1]{1}): (new byte[1]{0});

Строка в байт []

public static byte[] StringToByteArray(String str) => str.AsByteArray();

BigInteger в Byte []

public static byte[] BigIntegerToByteArray(BigInteger bigInteger) => bigInteger.AsByteArray();

Байт в Байт []

Вы можете подумать, что следующий фрагмент хорош:

public static byte[] Byte2Bytes(byte b) => new byte[1] { b };//WRONG IMPLEMENTATION!!!

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

Операторы и ключевые слова

Как упоминалось в официальном документе, NeoVM поддерживает большинство операторов и ключевых слов C #. Вот дополнительная информация:

Bool: И, ИЛИ и НЕ

Операторы «&&», «||» и "!" поддерживаются.

bool b = true;
bool a = false;
Runtime.Notify(!b, b && a, b || a);// Message of  false, false, true

Ключевое слово: «ref» и «out»

Ключевые слова «ref» или «out» - это возможности C #, позволяющие передавать локальные переменные в функцию в качестве ссылок. Эти ключевые слова не поддерживаются в смарт-контракте Neo.

Ключевое слово: «попытаться поймать», «бросить», «наконец»

Ключевые слова обработки исключений не поддерживаются.

Массив байтов: конкатенация и подмассив

//Concatenation
public static byte[] JoinByteArrays(byte[] ba1, byte[] ba2) => ba1.Concat(ba2);
//Get Byte array's subarray
public static byte[] SubBytes(byte[] data, int start, int length) => Helper.Range(data, start, length);

Ключевое слово «Это» в параметре

Иногда вы хотите определить расширение типов, чтобы сделать логику более сжатой и интуитивно понятной. NeoVM поддерживает ключевое слово «Это». В следующем примере кода показано, как его использовать.

// Write a static class for the extentions of byte array
public static class ByteArrayExts{
   // Return The subarray
   public static byte[] Sub(this byte[] bytes, int start, int len){
      return Helper.Range(bytes, start, len);
   }
   // Return the reversed bytearray
   public static byte[] Reverse(this byte[] bytes){
      byte[] ret = new byte[0];
      for(int i = bytes.Length -1 ; i>=0 ; i--){
         ret = ret.Concat(bytes.Sub(i,1));
      }
      return ret;
   }
}

Чтобы использовать вышеуказанные функции:

byte[] ba0 = {1,31,41,111};
byte[] ba1 = {12,6,254,0,231};
//Calls the Reverse and Sub functions with only one line.
Runtime.Notify(ba0, ba1, ba0.Reverse(), ba1.Sub(1,2));
//Call the extension functions multiple times in a row.
Runtime.Notify(ba1.Sub(0,3).Reverse());

Байтовый массив: измените значение

NeoVM не поддерживает побайтовые манипуляции с переменными. Итак, нам нужно разделить подмассивы, изменить некоторые части, а затем объединить их обратно. Следующая функция должна быть помещена в вышеуказанный класс ByteArrayExts.

public static class ByteArrayExts{
   //... previous functions ...
   
   public static byte[] Modify(this byte[] bytes, int start, byte[] newBytes){
      byte[] part1 = bytes.Sub(0,start);
      int endIndex = newBytes.Length + start;
      if(endIndex < bytes.Length){
         byte[] part2 = bytes.Sub(endIndex, bytes.Length-endIndex);
         return part1.Concat(newBytes).Concat(part2);
      }
      else{
         return part1.Concat(newBytes);
      }
   }
}

Использование:

byte[] orig = new byte[5]{1,2,3,4,5};
byte[] newValue = new byte[2]{6,7};
//Replace the 3rd and 4th elements of orig byte array.
byte[] ret = orig.Modify(2, newValue);//return {1,2,6,7,5};

Хранилище

Класс Storage / StorageMap - это единственные способы взаимодействия с постоянной информацией вашего смарт-контракта в цепочке. Основные операции CRUD:

//Create and Update: 1GAS/KB
Storage.Put(Storage.CurrentContext, key, value);
//Read: 0.1GAS/time
Storage.Get(Storage.CurrentContext, key);
//Delete: 0.1GAS/time
Storage.Delete(Storage.CurrentContext, key);

Вот несколько советов по использованию вышеуказанных функций:

  1. Перед вызовом Storage.Put () проверьте, не изменилось ли значение. Это сэкономит 0,9 ГАЗА, если оно не изменится.
  2. Перед вызовом Storage.Put () проверьте, пусто ли новое значение. Если пусто, используйте вместо него Storage.Delete (). Это также сэкономит 0,9 ГАЗА.
byte[] orig = Storage.Get(Storage.CurrentContext, key);
if (orig == value) return;//Don't invoke Put if value is unchanged. 
 
if (value.Length == 0){//Use Delete rather than Put if the new value is empty.
   Storage.Delete(Storage.CurrentContext, key);
}
else{
   Storage.Put(Storage.CurrentContext, key, value);
}

3. Разработайте структуру данных с приблизительной длиной, близкой к n КБ. Потому что функция записи 2Bytes стоит столько же, как и функция для 900Bytes. При необходимости можно даже комбинировать некоторые предметы.

BigInteger[] userIDs = //....Every ID takes constantly 32 Bytes.
int i = 0;
BigInteger batch = 0;
while( i< userIDs.Length){
   byte[] record = new byte[0];
   for(int j = 0; j< 31;j++){//31x32 = 992 Bytes. 
      int index = i + j;
      if( index == userIDs.Length ) return;
      else{
         record=record.Concat(userIDs[index].AsByteArray());
      }
   }
   //This cost only 1GAS rather than 31GAS.
   Storage.Put(Storage.CurrentContext, batch.AsByteArray(), record);
   batch = batch + 1;
   ++i;
}

Случайность

Генерация случайных значений - сложная задача для смарт-контрактов.

Во-первых, начальное значение должно быть детерминированным значением, связанным с блокчейном. В противном случае бухгалтеры не могут согласиться с этим. Большинство Dapps выбрали бы хеш-блокировку в качестве начального значения. Но при таком подходе разные пользователи, вызывающие одну и ту же функцию SC во время одного и того же блока, будут возвращать один и тот же результат. В статье Фабио Кардосо представлен новый алгоритм, использующий как хеш-блокировку, так и идентификатор транзакции.

Для некоторых высокочувствительных Dapps профессиональные пользователи могут возразить, что бухгалтеры могут вмешаться в хеширование блоков путем переупорядочения транзакций. В этом случае алгоритм Асимментропии предоставляется generalkim00 и maxpown3r. Процесс немного сложен, поэтому, пожалуйста, просто прочитайте исходный код смарт-контракта на примере лотереи ЗДЕСЬ, чтобы узнать.

Резюме

Спасибо, что прочитали это руководство. Я продолжу обновлять его, когда мы дадим больше советов по разработке смарт-контрактов. Благодарим dprat0821 за помощь в обсуждениях. Благодарим Fabio, generalkim00 и maxpown3r за отличные идеи.

Моя команда работает над игрой, чтобы вырезать искренние слова людей на блокчейне NEO. Спасибо за любые комментарии и предложения.

Адрес для пожертвований NEO: AKJEavjHZ3v96kxh7nWKpt4nVCj7VtirCg