老鱼 avatar

GUSD多重签名

chaimyu

Published: 07 Nov 2018 › Updated: 07 Nov 2018GUSD多重签名

GUSD多重签名

前面分析过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合约中了解了以太坊进行多重签名的方法,可以通过多重签名保证合约方法调用的安全。

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

Leave GUSD多重签名 to:

Written by

One day sitting in the well looked at the sky, blowing the wind uphole, jumping out of the well, dancing in the wind!

Read more #gusd posts


Best Posts From 老鱼

We have not curated any of chaimyu's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From 老鱼