引子
從上一節中,我們知道在EOS系統中帳戶必需遵循以兩個準則
- 必須短於13個字符
- 僅能包含以下字符:.12345abcdefghijklmnopqrstuvwxyz
EOS爲什麼要這樣做呢,這樣做有什麼好處,在EOS源碼中又是如何實現的,下面我們從EOS源碼中一步步分析,並解答這些疑問。
帳戶別名
在EOS源碼中,帳戶的類型是account_name,但account_name在C++代碼中又是什麼類型呢,追蹤代碼發現,EOS好多類型都用了別名機制,在源碼文件types.hpp中的133行,其代碼所示
using action_name = name; //合約操作名稱
using scope_name = name; //作用範圍名稱
using account_name = name; //帳戶名稱
using permission_name = name; //權限名稱
using table_name = name; //數據庫表名稱
這些名稱都指向同一個name,也就是說帳戶等其它名稱限制都由name類來完成的,下面我們看一下name類的實現
struct name {
uint64_t value = 0;
bool empty()const { return 0 == value; }
bool good()const { return !empty(); }
name( const char* str ) { set(str); }
name( const string& str ) { set( str.c_str() ); }
void set( const char* str );
template<typename T>
name( T v ):value(v){}
name(){}
explicit operator string()const;
string to_string() const { return string(*this); }
name& operator=( uint64_t v ) {
value = v;
return *this;
}
name& operator=( const string& n ) {
value = name(n).value;
return *this;
}
name& operator=( const char* n ) {
value = name(n).value;
return *this;
}
friend std::ostream& operator << ( std::ostream& out, const name& n ) {
return out << string(n);
}
friend bool operator < ( const name& a, const name& b ) { return a.value < b.value; }
friend bool operator <= ( const name& a, const name& b ) { return a.value <= b.value; }
friend bool operator > ( const name& a, const name& b ) { return a.value > b.value; }
friend bool operator >=( const name& a, const name& b ) { return a.value >= b.value; }
friend bool operator == ( const name& a, const name& b ) { return a.value == b.value; }
friend bool operator == ( const name& a, uint64_t b ) { return a.value == b; }
friend bool operator != ( const name& a, uint64_t b ) { return a.value != b; }
friend bool operator != ( const name& a, const name& b ) { return a.value != b.value; }
operator bool()const { return value; }
operator uint64_t()const { return value; }
operator unsigned __int128()const { return value; }
};
從name類中只有一個屬性value且類型爲uint64_t,即無符號64位整型,這是如何存儲13位字符的呢?它們之間是如何轉換的?等一系列疑問又產生了。
在name類重載了所有比較運算符用於名稱之間的比較,還寫了賦值構造和拷貝構造函數用於name類型的構造與賦值。比較重要的函數是set函數和to_string函數。這兩個函數應該是實現了字符到整型及整型到字符串的轉化。
設置帳戶名稱set
void name::set( const char* str ) {
const auto len = strnlen(str, 14);
EOS_ASSERT(len <= 13, name_type_exception, "Name is longer than 13 characters (${name}) ", ("name", string(str)));
value = string_to_name(str);
EOS_ASSERT(to_string() == string(str), name_type_exception,
"Name not properly normalized (name: ${name}, normalized: ${normalized}) ",
("name", string(str))("normalized", to_string()));
}
從代碼中可以看帳戶的名稱長度必須最大長度爲13位,大於13位會導致代碼斷言錯誤並向系統報告名稱超過13個字符的異常。然後調用了函數string_to_name將字符轉化爲整型,其函數string_to_name如下所示
static constexpr uint64_t string_to_name( const char* str )
{
uint64_t name = 0;
int i = 0;
for ( ; str[i] && i < 12; ++i) {
// NOTE: char_to_symbol() returns char type, and without this explicit
// expansion to uint64 type, the compilation fails at the point of usage
// of string_to_name(), where the usage requires constant (compile time) expression.
name |= (char_to_symbol(str[i]) & 0x1f) << (64 - 5 * (i + 1));
}
// The for-loop encoded up to 60 high bits into uint64 'name' variable,
// if (strlen(str) > 12) then encode str[12] into the low (remaining)
// 4 bits of 'name'
if (i == 12)
name |= char_to_symbol(str[12]) & 0x0F;
return name;
}
static constexpr uint64_t char_to_symbol( char c ) {
if( c >= 'a' && c <= 'z' )
return (c - 'a') + 6;
if( c >= '1' && c <= '5' )
return (c - '1') + 1;
return 0;
}
從代碼中可以看出char_to_symbol是將一個字符轉換爲一個64位的無符號整型,在此判斷判斷了字符必須爲a-z和1-5的字符。並將1-5的字符與數值1-5所對應,而字符a-z字符與數值6-31,對於字符.則對應數值爲0.從而將所有帳戶合法字符都轉化爲一個數值。 在string_to_name函數中,採用循環遍歷帳戶中的所有字符,並調用char_to_symbol函數將帳戶中的字符轉化所對應的數值,然後只取低5位,左移64- 5 * (i + 1)位,並與name做|操作。最後將所有字符所對應的數值都存儲到name類型中。從中可以計算一下帳戶總共有32個字符,所對應的數值爲0到31,在計算機中採用二進制表示數據,0到31只需要5位就可以存儲。一個64位的無符號整型最多可以存儲64/5=12,餘4位,也就是說最多13個字符且不能以字符z結尾,否則可能會出現問題。當不夠13字符時則以字符.補充。
將name轉化可見帳戶
name::operator string()const {
static const char* charmap = ".12345abcdefghijklmnopqrstuvwxyz";
string str(13,'.');
uint64_t tmp = value;
for( uint32_t i = 0; i <= 12; ++i ) {
char c = charmap[tmp & (i == 0 ? 0x0f : 0x1f)];
str[12-i] = c;
tmp >>= (i == 0 ? 4 : 5);
}
boost::algorithm::trim_right_if( str, []( char c ){ return c == '.'; } );
return str;
}
將name變量轉化爲字符串,首先將合法字符按其所對應的數值大小排列並賦值給charmap變量,並作爲查找表,然後在name變量的屬性value中依次取5位(第一次取4位),並按其值在查找表中找出對應的字符,從而還原出對應可見帳戶名稱,在帳戶規則中小於13位字符時高位用字符.來補充,所以帳戶最高位不能是字符.,將補充的字符.移除,從而完成帳戶還原。
總結
綜上所述,這就是EOS帳戶名稱有所限制的原因,它需要將13個字符裝入一個64位的無符號整型,且能將整型轉化爲所對應的字符帳戶的過程。
優點:
1、減少內存使用,帳戶13個字符存儲需要14個字節,而採用name變量存儲只需要8個字節。
2、程序運行速度快,在做帳戶查找與比較時,整型比較的效率會大大高於字符串比較。
缺點:
1、減少了帳戶名稱取名的最大範圍,比較不能有特殊字符和67890等字符。
2、對帳戶名稱的長度有所限制。
鏈接
星河公鏈
瞭解最新更多區塊鏈相關文章
敬請關注星鏈微信公衆號,星鏈與你共成長