Skip to content

Latest commit

 

History

History
307 lines (233 loc) · 10 KB

File metadata and controls

307 lines (233 loc) · 10 KB

28. Hash

雜湊函數(hash function)是一個密碼學概念,它可以將任意長度的訊息轉換為一個固定長度的值,這個值也稱為雜湊(hash)。

hash 特點

一個好的哈希函數應該具有以下幾個特性:

  • 單向性:從輸入的訊息到它的雜湊的正向運算簡單且唯一確定,而反過來非常難,只能靠暴力枚舉。
  • 靈敏度:輸入的訊息改變一點對它的哈希改變很大。
  • 高效率:從輸入的訊息到哈希的運算高效率。
  • 均一性:每個雜湊值被取到的機率應該基本上相等。
  • 抗碰撞性:
    • 弱抗碰撞性:給定一個訊息x,找出另一個訊息x',使得hash(x) = hash(x')
    • 是困難的。強抗碰撞性:找到任意x和x',使得hash(x) = hash(x')是困難的。

hash 應用

  • 產生資料唯一標識
  • 加密簽名
  • 安全加密

Keccak256函數是Solidity中最常用的雜湊函數,用法非常簡單:

哈希 = keccak256(数据);

Keccak256和SHA3

這是一個很有趣的事:

  • 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 值相同

29. 函数选择器Selector

函數選擇

發送的calldata中前"4個位元組"是selector(函數選擇器)。這一講,我們將介紹selector是什麼,以及如何使用。

msg.dataSolidity中的一個全域變量,值為完整的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、selector和函數簽名

method id定義為函数签名的Keccak雜湊後的前4個位元組,當selector與method id相符時,即表示呼叫該函數,那麼函数签名是什麼?

舉個例子,上面程式碼中mint的函數簽名為"mint(address)"。在同一個智能合約中,不同的函數有不同的函數簽名,因此我們可以透過函數簽名來確定要呼叫哪個函數。

注意,在函數簽名中,uint和int要寫為uint256和int256。

計算method id時,需要透過函數名稱和函數的參數類型來計算。

類型:

  1. 基礎類型參數
    // 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)"));
    }
  1. 基礎類型參數
    // 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])"));
    }
  1. 動態類型參數
    // 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)"));
    }
  1. 映射類型
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 來呼叫函數

30. Try Catch

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 的味道,但又不是,感覺是有個雞底下去做錯誤的呈現