GUSD多重签名

in #gusd5 years ago

前面分析过GUSD的一些功能和实现,其中还有一个重要部分就是Custodian合约,这个合约实现了多重签名机制,并且GUSD的其它一些安全特色,如时间锁定、操作取消也是通过这个合约来实现的。

Custodian合约概要

  1. 实现了2/N的签名机制,就是N个签名者只要有两个签名就可以通过回调机制调用GUSD合约的方法,如修改Custodian地址、增加发行量等;
  2. 合约提供了时间锁机制,主帐号有一个缺省的时间锁,其它请求方支持1ETH同时有一个扩展的时间锁;
  3. 合约提供了回退取消机制,一个已经完成的请求会阻止那些共享同一回调的正在进行的请求。

数据结构

Request

    struct Request {
        bytes32 lockId;
        bytes4 callbackSelector; // bytes4 and address can be packed into 1 word
        address callbackAddress;
        uint256 idx;
        uint256 timestamp;
        bool extended;
    }

lockId 请求的lockId号,例如gusd合约方法requestCustodianChange()返回的lockId

callbackSelector和callbackAddress,确定调用的合约和合约中函数,调用方式:callbackAddress.call(callbackSelector, lockId)

构造函数

    function Custodian(
        address[] _signers,
        uint256 _defaultTimeLock,
        uint256 _extendedTimeLock,
        address _primary
    )

创建合约时需要传入签名地址列表、缺省锁定时间、扩展锁定时间、主地址。

requestUnlock

    function requestUnlock(
        bytes32 _lockId,
        address _callbackAddress,
        bytes4 _callbackSelector,
        address _whitelistedAddress
    )
        public
        payable
        returns (bytes32 requestMsgHash)
    {
        require(msg.sender == primary || msg.value >= 1 ether);

        // disallow using a zero value for the callback address
        require(_callbackAddress != address(0));

        uint256 requestIdx = ++requestCount;
        // compute a nonce value
        // - the blockhash prevents prediction of future nonces
        // - the address of this contract prevents conflicts with co-operating contracts using this scheme
        // - the counter prevents conflicts arising from multiple txs within the same block
        uint256 nonce = uint256(keccak256(block.blockhash(block.number - 1), address(this), requestIdx));

        requestMsgHash = keccak256(nonce, _whitelistedAddress, uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF));

        requestMap[requestMsgHash] = Request({
            lockId: _lockId,
            callbackSelector: _callbackSelector,
            callbackAddress: _callbackAddress,
            idx: requestIdx,
            timestamp: block.timestamp,
            extended: false
        });

        // compute the expiry time
        uint256 timeLockExpiry = block.timestamp;
        if (msg.sender == primary) {
            timeLockExpiry += defaultTimeLock;
        } else {
            timeLockExpiry += extendedTimeLock;

            // any sender that is not the creator will get the extended time lock
            requestMap[requestMsgHash].extended = true;
        }

        emit Requested(_lockId, _callbackAddress, _callbackSelector, nonce, _whitelistedAddress, requestMsgHash, timeLockExpiry);
    }

这是custodian合约中一个主要函数,发送一个解锁的请求,类似于bitshares中发一个提案,只有有了提案后才能对提案进行签名。

第一行代码有意思,就是 require(msg.sender == primary || msg.value >= 1 ether); 要不是主帐户要不就支付1ETH可以发送这个请求,既有主帐户的安全控制,也可以灵活使用其它帐户,需要1ETH可以阻止恶意请求。

在Request中还有三个参数没有说明,在这意思也明了了:

idx: 请求次数,就是++requestCount

timestamp:块时间

extended:如果是主帐户操作为false,否则为true

而nonce值和_whitelistedAddress用来生成map的hash键,同时生成的这个requestMsgHash很重要,在以后的签名函数(completeUnlock)中都需要使用,可以提供这个数据给签名者进行离线签名。

completeUnlock

    function completeUnlock(
        bytes32 _requestMsgHash,
        uint8 _recoveryByte1, bytes32 _ecdsaR1, bytes32 _ecdsaS1,
        uint8 _recoveryByte2, bytes32 _ecdsaR2, bytes32 _ecdsaS2
    )
        public
        returns (bool success)
    {
        Request storage request = requestMap[_requestMsgHash];

        // copy storage to locals before `delete`
        bytes32 lockId = request.lockId;
        address callbackAddress = request.callbackAddress;
        bytes4 callbackSelector = request.callbackSelector;

        // failing case of the lookup if the callback address is zero
        require(callbackAddress != address(0));

        // reject confirms of earlier withdrawals buried under later confirmed withdrawals
        require(request.idx > lastCompletedIdxs[callbackAddress][callbackSelector]);

        address signer1 = ecrecover(_requestMsgHash, _recoveryByte1, _ecdsaR1, _ecdsaS1);
        require(signerSet[signer1]);

        address signer2 = ecrecover(_requestMsgHash, _recoveryByte2, _ecdsaR2, _ecdsaS2);
        require(signerSet[signer2]);
        require(signer1 != signer2);

        if (request.extended && ((block.timestamp - request.timestamp) < extendedTimeLock)) {
            emit TimeLocked(request.timestamp + extendedTimeLock, _requestMsgHash);
            return false;
        } else if ((block.timestamp - request.timestamp) < defaultTimeLock) {
            emit TimeLocked(request.timestamp + defaultTimeLock, _requestMsgHash);
            return false;
        } else {
            if (address(this).balance > 0) {
                // reward sender with anti-spam payments
                // ignore send success (assign to `success` but this will be overwritten)
                success = msg.sender.send(address(this).balance);
            }

            // raise the waterline for the last completed unlocking
            lastCompletedIdxs[callbackAddress][callbackSelector] = request.idx;
            // and delete the request
            delete requestMap[_requestMsgHash];

            // invoke callback
            success = callbackAddress.call(callbackSelector, lockId);

            if (success) {
                emit Completed(lockId, _requestMsgHash, signer1, signer2);
            } else {
                emit Failed(lockId, _requestMsgHash, signer1, signer2);
            }
        }
    }

completeUnlock函数用来对请求进行签名,参数包括requestUnlock()函数中的requestMsgHash,还有ECDSA签名数据(ECDSA签名验证单独再分析)。

在Custodian合约概要中说过,合约提供了回退取消机制,就如下两句代码:

        require(request.idx > lastCompletedIdxs[callbackAddress][callbackSelector]);
        ...
        lastCompletedIdxs[callbackAddress][callbackSelector] = request.idx;

只要签名通过后,request.idx就会记录下来,调用同样合约同样方法的请求在它之前的request.idx就会失效,这样可以防止更新Custodian之类操作前面的请求替换已经通过的请求。话说bitshares就碰到过好几次更新数据后发现数据又恢复原状了,后来才找到原因,原来是有多个提案修改帐号数据,但是提案更新不是增量更新而是覆盖更新,所以修改过的数据又被覆盖了。

合约提供了时间锁机制,也是在这个函数中做的控制,没有达到时间的请求不会被执行。

如果签名都通过,就会调用 callbackAddress.call(callbackSelector, lockId) 执行其它合约中的方法。

deleteUncompletableRequest

这个函数删除不可能完成的请求,就是前面说的idx小于已签名的idx的请求。

extendRequestTimeLock

这个函数会把主帐号的请求时间锁设成与扩展时间锁一样,有什么作用呢?防止过早被签名者确认?

Custodian部署

PrintLimiter

PrintLimiter的Custodian地址:0x1789cca7430aacbdb7c89f9b5695a9c06e4764eb

看看PrintLimiter的Custodian数据:

defaultTimeLock: 3600(1小时)

extendedTimeLock: 86400(1天)

requestCount: 0

也就是这个合约创建后截止2018年11月5日,还没有调用过增加PrintLimiter天花板的方法。

ERC20Proxy

ERC20Proxy的Custodian地址:0x9a7b5f6e453d0cda978163cb4a9a88367250a52d

defaultTimeLock: 172800(2天)

extendedTimeLock: 604800(7天)

requestCount: 4

这个合约的解锁方法是调用过4次,看看调用哪些方法。


从etherscan可以看到4次调用的都是如下方法:

Function: requestUnlock(bytes32 _lockId, address _callbackAddress, bytes4 _callbackSelector, address _whitelistedAddress)

看看具体的一个交易:

056fd409e1d7a124bd7017459dfea2f387b6d5cd这个就是gusd合约地址,8181b029就是其中一个方法,这个是什么方法也是可以分析出来的。

还有交易调用的是0xc42B14e49744538e3C239f8ae48A1Eaaf35e68a0,这个是ERC20Store合约地址。

调用者地址0xd24400ae8BfEBb18cA49Be86258a3C749cf46853,也是调用参数中白名单地址,也是合约的primary地址,在etherscan看是Gemini_1。

结论

我们对GUSD的多重签名进行了比较详细的剖析,从Custodian合约中了解了以太坊进行多重签名的方法,可以通过多重签名保证合约方法调用的安全。

同样以太坊的多重签名地址也是用合约实现的,有时间再去看看以太坊的多重签名实现。

Sort:  

Hello! Your post has been resteemed and upvoted by @ilovecoding because we love coding! Keep up good work! Consider upvoting this comment to support the @ilovecoding and increase your future rewards! ^_^ Steem On!

Reply !stop to disable the comment. Thanks!