本教程分享了一些實用的技巧供您參考。 開發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複製程式碼
關鍵字: “ref” 和 “out”
關鍵字“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。 在這種情況下,generalkim00和maxpown3r提供了非對稱熵的演算法。 這個過程有點複雜,所以要想學習的話,可以點選這個連結閱讀他們這個博彩的例子的智慧合約原始碼。
總結
感謝閱讀本教程。如果我們在開發智慧合約時發現更多技巧的話,我會繼續在這裡更新它。感謝dprat0821在討論中給予的幫助。感謝Fabio, generalkim00和maxpown3r的精彩想法。
我的團隊正在開發一款將人們內心深處的話語刻在NEO區塊鏈上的遊戲。謝謝你的意見和建議。
NEO 捐贈
地址: AKJEavjHZ3v96kxh7nWKpt4nVCj7VtirCg
翻譯:包子