主页 > imtoken安卓官网 > 以太坊DAO攻击解决方案代码分析
以太坊DAO攻击解决方案代码分析
虽然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;
}
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个区块,是为了防止矿工用预留信息污染非分叉区块,方便轻节点安全同步数据。 同时,所有节点在验证区块头时必须安全地验证特殊字段信息,验证该区块是否属于正确的分叉。
// 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秒倒计时,倒计时结束后断开连接。
// 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()
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) {
// ...
// 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 攻击: