雜湊函數(hash function)是一個密碼學概念,它可以將任意長度的訊息轉換為一個固定長度的值,這個值也稱為雜湊(hash)。
一個好的哈希函數應該具有以下幾個特性:
- 單向性:從輸入的訊息到它的雜湊的正向運算簡單且唯一確定,而反過來非常難,只能靠暴力枚舉。
- 靈敏度:輸入的訊息改變一點對它的哈希改變很大。
- 高效率:從輸入的訊息到哈希的運算高效率。
- 均一性:每個雜湊值被取到的機率應該基本上相等。
- 抗碰撞性:
- 弱抗碰撞性:給定一個訊息x,找出另一個訊息x',使得hash(x) = hash(x')
- 是困難的。強抗碰撞性:找到任意x和x',使得hash(x) = hash(x')是困難的。
- 產生資料唯一標識
- 加密簽名
- 安全加密
Keccak256函數是Solidity中最常用的雜湊函數,用法非常簡單:
哈希 = keccak256(数据);
這是一個很有趣的事:
-
sha3由keccak標準化而來,在許多場合下Keccak和SHA3是同義詞,但在2015年8月SHA3最終完成標準化時,NIST調整了填充演算法。所以SHA3就和keccak計算的結果不一樣,這點在實際開發上要注意。
-
以太坊在開發的時候sha3還在標準化中,所以採用了keccak,所以Ethereum和Solidity智能合約程式碼中的SHA3是指Keccak256,而不是標準的NIST-SHA3,為了避免混淆,直接在合約程式碼中寫成Keccak256是最清晰的。
🧑💻 Keccak256 才是 eth 的標準
function hash(
uint _num,
string memory _string,
address _addr
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_num, _string, _addr));
}
我們用keccak256示範一下之前講到的弱抗碰撞性,即給定一個訊息x,找到另一個訊息x',使得hash(x) = hash(x')是困難的。
我們給定一個訊息0xAA,試圖去找另一個訊息,使得它們的哈希值相等:
// 弱抗碰撞性
function weak(
string memory string1
)public view returns (bool){
return keccak256(abi.encodePacked(string1)) == _msg;
}
Solidity
最常用的雜湊函數keccak256
hash 值如果相同,代表下載的檔案是一樣的(原來這句話是有待商議的) // 原因
- 有可能是不同的檔案,但是 hash 值相同
發送的calldata
中前"4個位元組"是selector(函數選擇器)
。這一講,我們將介紹selector
是什麼,以及如何使用。
msg.data
是Solidity
中的一個全域變量,值為完整的calldata
(呼叫函數時傳入的資料)。
在下面的程式碼中,我們可以透過Log事件
來輸出呼叫mint
函數的calldata
:
// event 返回msg.data
event Log(bytes data);
function mint(address to) external{
emit Log(msg.data);
}
當參數0x2c44b726ADF1963cA47Af88B284C06f30380fC78為時,輸出的calldata為
0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78
切割一下
前4个字节为函数选择器selector:
0x6a627842
后面32个字节为输入的参数:
0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78
其實calldata就是告訴智能合約,我要呼叫哪個函數,以及參數是什麼。
method id
定義為函数签名的Keccak雜湊後的前4個位元組
,當selector與method id相符時,即表示呼叫該函數,那麼函数签名是什麼?
舉個例子,上面程式碼中mint的函數簽名為"mint(address)"
。在同一個智能合約中,不同的函數有不同的函數簽名,因此我們可以透過函數簽名來確定要呼叫哪個函數。
注意,在函數簽名中,uint和int要寫為uint256和int256。
計算method id時,需要透過函數名稱和函數的參數類型來計算。
類型:
- 基礎類型參數
// elementary(基础)类型参数selector
// 输入:param1: 1,param2: 0
// elementaryParamSelector(uint256,bool) : 0x3ec37834
function elementaryParamSelector(uint256 param1, bool param2) external returns(bytes4 selectorWithElementaryParam){
emit SelectorEvent(this.elementaryParamSelector.selector);
return bytes4(keccak256("elementaryParamSelector(uint256,bool)"));
}
- 基礎類型參數
// fixed size(固定长度)类型参数selector
// 输入: param1: [1,2,3]
// fixedSizeParamSelector(uint256[3]) : 0xead6b8bd
function fixedSizeParamSelector(uint256[3] memory param1) external returns(bytes4 selectorWithFixedSizeParam){
emit SelectorEvent(this.fixedSizeParamSelector.selector);
return bytes4(keccak256("fixedSizeParamSelector(uint256[3])"));
}
- 動態類型參數
// non-fixed size(可变长度)类型参数selector
// 输入: param1: [1,2,3], param2: "abc"
// nonFixedSizeParamSelector(uint256[],string) : 0xf0ca01de
function nonFixedSizeParamSelector(uint256[] memory param1,string memory param2) external returns(bytes4 selectorWithNonFixedSizeParam){
emit SelectorEvent(this.nonFixedSizeParamSelector.selector);
return bytes4(keccak256("nonFixedSizeParamSelector(uint256[],string)"));
}
- 映射類型
contract DemoContract {
// empty contract
}
contract Selector{
// Struct User
struct User {
uint256 uid;
bytes name;
}
// Enum School
enum School { SCHOOL1, SCHOOL2, SCHOOL3 }
...
// mapping(映射)类型参数selector
// 输入:demo: 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99, user: [1, "0xa0b1"], count: [1,2,3], mySchool: 1
// mappingParamSelector(address,(uint256,bytes),uint256[],uint8) : 0xe355b0ce
function mappingParamSelector(DemoContract demo, User memory user, uint256[] memory count, School mySchool) external returns(bytes4 selectorWithMappingParam){
emit SelectorEvent(this.mappingParamSelector.selector);
return bytes4(keccak256("mappingParamSelector(address,(uint256,bytes),uint256[],uint8)"));
}
...
}
// 使用selector来调用函数
function callWithSignature() external{
...
// 调用elementaryParamSelector函数
(bool success1, bytes memory data1) = address(this).call(abi.encodeWithSelector(0x3ec37834, 1, 0));
...
}
call method_id
來呼叫函數
try-catch
是現代程式語言幾乎都有的處理異常的一種標準方式,Solidity0.6
版本也添加了它
在Solidity中,try-catch只能被用於external函數
或創建合約時constructor(被視為external函數)
的呼叫。基本語法如下:
try externalContract.f() {
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}
其中externalContract.f()
是某個外部合約的函數調用,try模組在調用成功的情況下運行,而catch模組則在調用失敗時運行。
同樣可以使用this.f()
來替代externalContract.f()
,this.f()
也被視作為外部調用,但不可在構造函數中使用,因為此時合約還未創建。
如果呼叫的函數有傳回值,那麼必須在try之後聲明returns(returnType val)
,並且在try模組中可以使用傳回的變數;如果是建立合約,那麼傳回值就是新建立的合約變數。
try externalContract.f() returns(returnType val){
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}
try externalContract.f() returns(returnType){
// call成功的情况下 运行一些代码
} catch Error(string memory /*reason*/) {
// 捕获revert("reasonString") 和 require(false, "reasonString")
} catch Panic(uint /*errorCode*/) {
// 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界
} catch (bytes memory /*lowLevelData*/) {
// 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支
// 例如revert() require(false) revert自定义类型的error
}
EX: OnlyEven 我們建立一個外部合約OnlyEven,並使用try-catch來處理異常:
contract OnlyEven{
constructor(uint a){
require(a != 0, "invalid number");
assert(a != 1);
}
function onlyEven(uint256 b) external pure returns(bool success){
// 输入奇数时revert
require(b % 2 == 0, "Ups! Reverting");
success = true;
}
}
// 成功event
event SuccessEvent();
// 失败event
event CatchEvent(string message);
event CatchByte(bytes data);
// 声明OnlyEven合约变量
OnlyEven even;
constructor() {
even = new OnlyEven(2);
}
// 在external call中使用try-catch
function execute(uint amount) external returns (bool success) {
try even.onlyEven(amount) returns(bool _success){
// call成功的情况下
emit SuccessEvent();
return _success;
} catch Error(string memory reason){
// call不成功的情况下
emit CatchEvent(reason);
}
}
// 在创建新合约中使用try-catch (合约创建被视为external call)
// executeNew(0)会失败并释放`CatchEvent`
// executeNew(1)会失败并释放`CatchByte`
// executeNew(2)会成功并释放`SuccessEvent`
function executeNew(uint a) external returns (bool success) {
try new OnlyEven(a) returns(OnlyEven _even){
// call成功的情况下
emit SuccessEvent();
success = _even.onlyEven(a);
} catch Error(string memory reason) {
// catch失败的 revert() 和 require()
emit CatchEvent(reason);
} catch (bytes memory reason) {
// catch失败的 assert()
emit CatchByte(reason);
}
}
個人感覺這裡的 try catch 更有 switch 的味道,但又不是,感覺是有個雞底下去做錯誤的呈現