開發NEO智慧合約的實用技巧

NEOGO發表於2018-12-11

本教程分享了一些實用的技巧供您參考。 開發C#NEO智慧合約的最大挑戰之一是NeoVM支援的語言特性,實際操作中使用的特性比官方文件提供的要多。 還有一些關於儲存互動與隨機生成的實用技巧。 Enjoy hacking.

型別轉換

NeoVM支援的基本型別是位元組陣列(Byte []),然後是常用的Boolean,String和BigInteger。 還有其他整數型別,如Int8,Int64,UInt16,long,ulong等,這些可以被隱式轉換為BigInteger。 Float型別不受支援。

所以我們只要關注Byte [],Boolean,String和BigInteger之間的轉換。 注意:有些轉換不是官方定義的,在這種情況下,我會嘗試做出最合理的實現。

Byte[] to Boolean

雖然這個看起來是最簡單的,但實際上它沒有直接轉換。官方說明中只提到False等於整數 0。我們假設True等於所有其他值,而空位元組陣列等於False。所以我們定義了以下幾個函式:

public static bool bybool (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
複製程式碼

Byte[] to String

這個轉換直接由Neo.SmartContract.Framework.Helper提供

public static string BytesToByte(byte[] data) => data.AsString();複製程式碼

Byte[] to BigInteger

public static BigInteger BytesToBigInteger(byte[] data) => data.AsBigInteger();複製程式碼

Boolean to Byte[]

這個也需要手工轉換。

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

String to Byte[]

public static byte[] StringToByteArray(String str) => str.AsByteArray();複製程式碼

BigInteger to Byte[]

public static byte[]
BigIntegerToByteArray(BigInteger bigInteger) => bigInteger.AsByteArray();複製程式碼

Byte to Byte[]

你可能會認為下面這段程式碼看起來很好:

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

它可以通過編譯,但在大多數情況下會返回意想不到的值。 這是因為不支援按照變數分配位元組陣列。 所以要避免使用這種轉換。

操作符和關鍵字

正如官方文件中提到的,NeoVM支援大多數的c#操作符和關鍵字。補充說明如下:

Bool: AND, OR NOT

支援操作符“&&”,“||” “!”

bool b = true;
bool a = false;
Runtime.Notify(!b, b && a, b || a);// 分別代表false, false, true複製程式碼

關鍵字: “refout

關鍵字“ref”或“out”是C#語言的特性,用來允許將區域性變數傳給函式作為引用。Neo智慧合約不支援這些關鍵字。

關鍵字: “try-catch”, “throw”, “finally

不支援這幾個用於異常處理的關鍵字

位元組陣列:級聯和子陣列

//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);複製程式碼

關鍵字 引數中的This

有時你需要定義型別的擴充套件,從而使邏輯更加簡潔直觀。 NeoVM支援關鍵字“This”。 以下示例程式碼顯示瞭如何使用它。

// 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.9GAS。

2.在呼叫Storage.Put()之前,檢查新值是否為空。 如果為空,請改用Storage.Delete()。 這也將節省0.9GAS。

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 KB。因為方法寫2位元組和寫900位元組的開銷是一樣的。如有必要,你甚至可以組合一些項。

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會選擇blockhash作為種子。但是使用這種方法的話,不同的使用者在同一個區塊中呼叫相同的SC方法會返回相同的結果。在Fabio Cardoso的文章中,引入了一種新的演算法來同時使用blockhash和transactionID作為種子。

對於一些高度敏感的Dapps,專業使用者可能會爭辯說,記賬員可以通過重新排序交易來干預blockhashes。 在這種情況下,generalkim00maxpown3r提供了非對稱熵的演算法。 這個過程有點複雜,所以要想學習的話,可以點選這個連結閱讀他們這個博彩的例子的智慧合約原始碼。

總結

感謝閱讀本教程。如果我們在開發智慧合約時發現更多技巧的話,我會繼續在這裡更新它。感謝dprat0821在討論中給予的幫助。感謝Fabio, generalkim00maxpown3r的精彩想法。

我的團隊正在開發一款將人們內心深處的話語刻在NEO區塊鏈上的遊戲。謝謝你的意見和建議。

NEO 捐贈

地址: AKJEavjHZ3v96kxh7nWKpt4nVCj7VtirCg

原文連結:medium.com/@gongxiaoji…

翻譯:包子


相關文章