主页 > imtoken安卓官网 > 以太坊DAO攻击解决方案代码分析

以太坊DAO攻击解决方案代码分析

imtoken安卓官网 2024-01-26 05:08:44

虽然2016年就发生了The DAO攻击,但是针对该攻击的解决方案还是值得学习的。

区块链本来就是一个去中心化的架构。 当以太坊第一次遭遇严重的智能合约黑客攻击时,采用的解决方案破坏了去中心化的概念。

是否违背区块链精神,这里不做讨论。 本文重点介绍该解决方案的技术实现细节。 该方案涉及网络隔离技术和矿工共识投票技术。 并且只是从软件上处理,没有破坏共识协议。 该方案的成功实施为区块链分叉提供了实践经验,值得公链开发者学习借鉴。

什么是 DAO 攻击

简单来说,2016年4月30日,一个名为“The DAO”的创业团队在以太坊上通过智能合约进行了ICO众筹。 在 28 天内,它筹集了 1.5 亿美元,成为历史上最大的众筹项目。

THE DAO 的创始人之一 Stephan TualTual 于 6 月 12 日宣布,他们在该软件中发现了一个“递归调用漏洞”。 不幸的是,当程序员正在解决这个问题和其他问题时,一位不知名的黑客开始使用这种途径从 THE DAO 代币销售中收集以太币。 6 月 18 日,黑客成功挖出超过 360 万枚以太币以太坊ico代码,并将其放入一个与 THE DAO 结构相同的 DAO 子组织中。

THE DAO 持有以太币总量的近 15%,因此此次 THE DAO 的问题对以太坊网络及其加密货币产生了负面影响。

6 月 17 日,以太坊基金会的 Vitalik Buterin 更新了一份重要报告。 他说DAO被攻击了,但是他已经想出了一个解决方案:

现在提出了一个软件分叉解决方案,通过它调用代码或委托调用的任何交易——使用代码 hash0x7278d050619a624f84f51987149ddb439cdaadfba5966f7cfaea7ad44340a4ba(又名 DAO 和子 DAO)减少账户余额——将被视为无效...

最终,由于社会上的不同意见,以太坊最终分裂成以太坊经典ETC,支持维持现状,并同意在以太坊现网实施软件分叉方案。

以上内容整理自文章The DAO attack [1]。

解决方案

由于投资者已经将以太币投入到 The DAO 合约或其子合约中,因此在攻击发生后无法立即撤回。 既要让投资者快速撤回投资,又要阻止黑客转移资产。

V神公布的解决方案是在程序中嵌入转账合约以太坊的代码以太坊ico代码,让矿工选择是否支持分叉。 当到达分叉点时,The DAO 及其子合约中的以太币被转移到一个新的安全提款合约中。 全部转账后,原投资人可以直接从提现合约中快速取回以太币。 提款合约在讨论方案时已经部署到主网。 合约地址为[0xbf4ed7b27f1d666546e30d74d50d173d20bca754][WithdrawDAO]。

提现合约代码如下:

// Deployed on mainnet at 0xbf4ed7b27f1d666546e30d74d50d173d20bca754
contract DAO {
    function balanceOf(address addr) returns (uint);
    function transferFrom(address from, address to, uint balance) returns (bool);
    uint public totalSupply;
}
contract WithdrawDAO {
    DAO constant public mainDAO = DAO(0xbb9bc244d798123fde783fcc1c72d3bb8c189413);
    address public trustee = 0xda4a4626d3e16e094de3225a751aab7128e96526;
    function withdraw(){
        uint balance = mainDAO.balanceOf(msg.sender);
        if (!mainDAO.transferFrom(msg.sender, this, balance) || !msg.sender.send(balance))
            throw;
    }

以太坊开源代码_以太坊ico代码_以太坊ico众筹

function trusteeWithdraw() { trustee.send((this.balance + mainDAO.balanceOf(this)) - mainDAO.totalSupply()); } }

同时,为了兼顾两大阵营,软件提供硬分叉切换,选择权交给社区。 支持分叉的矿工在从区块X到区块X+9出块时,会在区块的extradata字段写入0x64616f2d686172642d666f726b(“dao-hard-fork”的十六进制数)。 从分叉点开始,如果连续10个区块有硬分叉投票,则表示硬分叉成功。

矿工投票和区块头验证

首先,选择权留给社区。 因此,是否同意硬分叉可以通过参数来选择。 但是在当前版本中,社区已经完成了一次硬分叉,所以switch类的代码已经被移除。

目前主网已配置默认支持DAO分叉,起始硬分叉高度设置为1920000,代码如下:

// params/config.go:38
MainnetChainConfig = &ChainConfig{ 
        DAOForkBlock:        big.NewInt(1920000),
        DAOForkSupport:      true, 
    }

如果矿工支持硬分叉,需要在区块头extradata 192000-192009高度写入指定信息0x64616f2d686172642d666f726b表示支持硬分叉。

//params/dao.go:28var DAOForkBlockExtra = common.FromHex("0x64616f2d686172642d666f726b")
// params/dao.go:32var DAOForkExtraRange = big.NewInt(10)

矿工在支持硬分叉时写入固定的投票信息:

// miner/worker.go:857
if daoBlock := w.config.DAOForkBlock; daoBlock != nil { 
    // 检查是否区块是否仍然属于分叉处理期间:[DAOForkBlock,DAOForkBlock+10)
    limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange)
    if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 {
        // 如果支持分叉,则覆盖Extra,写入保留的投票信息
        if w.config.DAOForkSupport {
        header.Extra = common.CopyBytes(params.DAOForkBlockExtra)
        } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) {
            // 如果矿工反对,则不能让其使用保留信息,覆盖它。
        header.Extra = []byte{}  
        }
    }
}

之所以要求连续10个区块,是为了防止矿工用预留信息污染非分叉区块,方便轻节点安全同步数据。 同时,所有节点在验证区块头时必须安全地验证特殊字段信息,验证该区块是否属于正确的分叉。

以太坊ico众筹_以太坊开源代码_以太坊ico代码

// consensus/ethash/consensus.go:294 
if err := misc.VerifyDAOHeaderExtraData(chain.Config(), header); err != nil { //❶
    return err
}
// consensus/misc/dao.go:47 
func VerifyDAOHeaderExtraData(config *params.ChainConfig, header *types.Header) error { 
    if config.DAOForkBlock == nil {//❷
        return nil
    }
    limit := new(big.Int).Add(config.DAOForkBlock, params.DAOForkExtraRange) //❸
    if header.Number.Cmp(config.DAOForkBlock) < 0 || header.Number.Cmp(limit) >= 0 {
        return nil
    }
    if config.DAOForkSupport {
        if !bytes.Equal(header.Extra, params.DAOForkBlockExtra) { //❹
            return ErrBadProDAOExtra
        }
    } else {
        if bytes.Equal(header.Extra, params.DAOForkBlockExtra) {//❺
            return ErrBadNoDAOExtra
        }
    }
    // All ok, header has the same extra-data we expect
    return nil
}

•❶ 校验区块头时增加DAO区块头识别校验。 • ❷ 如果节点没有设置分叉点,则不会被验证。 •❸ 确保DAO 分叉点只有10 个区块被验证。 •❹如果节点允许分叉,则区块头Extra必须满足要求。 •❺ 当然,如果节点不允许分叉,也不能在区块头中额外添加非分叉链的特殊信息。

这个config.DAOForkBlock开关类似于互联网公司灰度推出新产品特性的功能开关。 在区块链上,可以先实现功能代码逻辑。 至于何时启用,最终开放时间可与社区和开发者协商确定。 当然,区块链上的区块高度就相当于时间戳。 比如DAO的分叉点1920000也是经过讨论敲定的。

如何分离网络?

如果分叉后网络不能快速分离,就会给节点带来奇怪的问题。 从长远来看,为了解决未来可能出现的分叉,应该设计一个通用的解决方案来降低代码噪音。 否则,您会发现代码中充满了各种模因。 但是时间很紧迫,这次DAO的fork处理是通过具体的代码拦截来实现的。

在我看来,区块链项目不同于其他传统软件。 一旦发现严重的bug,那是非常致命的。 上线后的代码修改应尽可能少,并进行全面测试。 非常赞同dao的代码处理。 没必要为未来可能的分叉做一个“伟大”的功能,务实地解决问题才是正道。

节点不能同时作为两个阵营的中继点,两个网络要分开,互不干扰。 DAO硬分叉的处理方式如下:节点连接握手后,向对方请求分叉区块头信息。 必须在 15 秒内响应,否则断开连接。

代码实现在eth/handler.go文件中,在消息层进行拦截处理。 节点握手后开始15秒倒计时,倒计时结束后断开连接。

以太坊ico众筹_以太坊开源代码_以太坊ico代码

// eth/handler.go:300
    p.forkDrop = time.AfterFunc(daoChallengeTimeout, func() {
        p.Log().Debug("Timed out DAO fork-check, dropping")
        pm.removePeer(p.id)
    })

倒计时前,需要向对方索取区块头信息,用于分叉验证。

// eth/handler.go:297
    if err := p.RequestHeadersByNumber(daoBlock.Uint64(), 1, 0, false); err != nil {
        return err
    }

这时候对方收到请求后,如果有这样的区块头就返回,否则忽略。

// eth/handler.go:348
    case msg.Code == GetBlockHeadersMsg:  
        var query getBlockHeadersData
        if err := msg.Decode(&query); err != nil {
            return errResp(ErrDecode, "%v: %v", msg, err)
        }
        hashMode := query.Origin.Hash != (common.Hash{})
        first := true
        maxNonCanonical := uint64(100) 
        var (
            bytes   common.StorageSize
            headers []*types.Header
            unknown bool
        )
        //省略一部分 ...
        return p.SendBlockHeaders(headers)

因此,出现了几种情况。 根据不同情况分别处理:

1.有一个返回块头:

如果返回的区块头不一致,则验证失败,等待倒计时结束。 如果区块头一致,则按照上述验证分叉区块的方法进行检查。 如果验证失败,则直接断开连接,说明已经属于不同的fork。 如果验证通过,则关闭倒计时,验证完成。

// eth/handler.go:465
if p.forkDrop != nil && pm.chainconfig.DAOForkBlock.Cmp(headers[0].Number) == 0 { 
                p.forkDrop.Stop()

以太坊ico众筹_以太坊开源代码_以太坊ico代码

p.forkDrop = nil if err := misc.VerifyDAOHeaderExtraData(pm.chainconfig, headers[0]); err != nil { p.Log().Debug("Verified to be on the other side of the DAO fork, dropping") return err } p.Log().Debug("Verified to be on the same side of the DAO fork") return nil }

1、没有返回区块头:

如果没有到达分叉高度,则不验证,假设双方在同一个网络中。 但是我已经到了叉子的高度,所以我考虑对方的TD是不是比我的叉子块高。 如果是,则包含,暂时认为属于同一个网络。 否则,验证失败。

// eth/handler.go:442 
if len(headers) == 0 && p.forkDrop != nil { 
    verifyDAO := true
    if daoHeader := pm.blockchain.GetHeaderByNumber(pm.chainconfig.DAOForkBlock.Uint64()); daoHeader != nil {
        if _, td := p.Head(); td.Cmp(pm.blockchain.GetTd(daoHeader.Hash(), daoHeader.Number.Uint64())) >= 0 {
            verifyDAO = false
        }
    } 
    if verifyDAO {
        p.Log().Debug("Seems to be on the same side of the DAO fork")
        p.forkDrop.Stop()
        p.forkDrop = nil
        return nil
    }
}

转让资产

以上都是一个安全稳定的硬分叉,隔离了两个网络。 硬分叉的目的是以人为干预的方式拦截攻击者的资产。 一旦到达分叉点,立即启动资产转移操作。 首先,当矿工挖到分叉点时,需要进行转账操作:


// miner/worker.go:877
func (w *worker) commitNewWork(interrupt *int32, noempty bool, timestamp int64) {
    // ...

以太坊ico代码_以太坊ico众筹_以太坊开源代码

// Create the current work task and check any fork transitions needed env := w.current if w.config.DAOForkSupport && w.config.DAOForkBlock != nil && w.config.DAOForkBlock.Cmp(header.Number) == 0 { misc.ApplyDAOHardFork(env.state) } // ... }

其次,当任何一个节点收到一个区块并进行本地处理验证时,也需要在分叉点执行:

// core/state_processor.go:66
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) {
    //...
    // Mutate the block and state according to any hard-fork specs
    if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
        misc.ApplyDAOHardFork(statedb)
    }
    //...
}

转移资金也通过提款合约处理。 将所有资金从 The DAO 合约(包括子合约)转移到新合约。

func ApplyDAOHardFork(statedb *state.StateDB) {
    // Retrieve the contract to refund balances into
    if !statedb.Exist(params.DAORefundContract) {
        statedb.CreateAccount(params.DAORefundContract)
    }
    // Move every DAO account and extra-balance account funds into the refund contract
    for _, addr := range params.DAODrainList() {
        statedb.AddBalance(params.DAORefundContract, statedb.GetBalance(addr))
        statedb.SetBalance(addr, new(big.Int))
    }
}

至此,所有合约资金都被强行转移到了新合约中。

参考

[1] DAO 攻击: