关于翻译:介绍在使用-Solidity-以太坊升级智能合约的挑战

41次阅读

共计 7759 个字符,预计需要花费 20 分钟才能阅读完成。

在开发软件的时候,咱们常常须要公布新的版本来减少新的性能或者修复 bug。当波及到智能合约开发时,也没有什么区别。尽管将智能合约更新到新版本通常不像更新其余类型的雷同复杂性的软件那么简略。

大多数区块链,尤其是像 Ethereum 这样的公链,都实现了不可变的个性。实践上不容许任何人扭转区块链的 “ 过来 ”。不可变更性实用于区块链中的所有交易,包含用于部署智能合约和相干代码的交易。换句话说,一旦智能合约的代码被部署到区块链上,它将永远 “ 原样 “ 地 “ 活着 ”– 没有人能够扭转它。如果发现了 bug 或者须要增加新的性能,咱们无奈间接批改部署合约的代码。

如果一个智能合约是不可更改的,那么你如何可能将它降级到新的版本?答案就在于将新的智能合约部署到区块链上。但这种办法会带来一些须要解决的挑战。最根本也是最常见的是,所有应用智能合约的用户都须要参考新合约版本的地址,第一个合同的版本应该被禁用,强制每个用户应用新版本。

通常,你须要确保旧版本的数据(状态)被迁徙或以某种形式提供给新版本。在最简略的状况下,这意味着你须要将旧版本中的状态复制 / 迁徙到新合同的版本中。

上面的章节将更具体地形容这些挑战。为了更好的阐明,咱们用上面两个版本的 MySmartContract 作为参考。

// Version 1
contract MySmartContract {
    uint32 public counter;
    constructor() public {counter = 0;}
    function incrementCounter() public {counter += 2; // This "bug" is intentional.}
}

// Version 2
contract MySmartContract {
    uint32 public counter;
    constructor(uint32 _counter) public {counter = _counter;}
    function incrementCounter() public {counter++;}
}

用户可参考新合同的地址。

当部署到区块链时,智能合约的每个实例都被调配到一个惟一的地址。该地址用于援用智能合约的实例,以便调用其办法并从 / 向合约的存储(状态)读取 / 写入数据。当你将合同的更新版本部署到区块链时,合同的新实例将部署在一个新的地址。这个新地址与第一个合约的地址不同。这意味着,所有与智能合约交互的用户、其余智能合约和 / 或 dApp(去中心化利用)都须要更新,以便它们应用更新版本的地址。剧透:有一些选项能够防止这个问题,你会在本节最初看到。

那么,让咱们思考以下状况。你用下面 Version 1 的代码创立 MySmartContract。它被部署到区块链的地址 A1(这不是一个实在的 Ethereum 地址 – 仅用于阐明目标)。所有想要与 Version 1 交互的用户都须要应用地址 A1 来援用它。当初,通过一段时间后,咱们留神到了办法 incrementCounter 中的 bug:它是以 2 来递增计数器,而不是以 1 来递增它,所以咱们实现了一个修复,产生了 MySmartContract 的 Version 2 版本。这个新合约的版本被部署到地址 D5 的区块链上。此时,如果用户想要与 Version 2 进行交互,须要应用地址 D5,而不是 A1。这就是为什么所有与 MySmartContract 交互的用户都须要更新,以便他们参考新的地址 D5 的起因。

你可能批准强制用户更新不是最好的办法,思考到更新智能合约的版本应该对应用它的用户尽可能的通明。有不同的策略能够用来解决这个问题。能够应用一些设计模式,如 Registry、不同类型的 Proxies 来使降级更容易,并为用户提供透明度。另一个很好的抉择是应用 Ethereum Name Service,并注册一个用户敌人的名字,解析到你的合约地址。有了这个抉择,合约的用户不须要晓得合约的地址,只须要晓得它的用友名。因而,降级到新的地址对你的合约用户来说将是通明的。具体采取何种策略,取决于智能合约的应用场景。

禁用旧版合约

咱们在上一节中理解到,所有用户都须要更新能力应用 Version 2 的地址(D5),或者咱们的合同应该实现某种机制,使这个过程对用户通明。尽管如此,如果你是合同的拥有者,你可能要执行所有用户只应用最新的 D5 版本。如果用户无心中或没有应用 A1,你要保障 Version 1 曾经被废除,无奈应用。

在这种状况下,你能够实现一种技术来进行 MySmartContract 的 Version 1。这个技术是由一个名为 Circuit Breaker 的设计模式实现的。它通常也被称为可暂停合同或紧急进行。

一般来说,Circuit Breaker 能够进行智能合约的性能。此外,它还能够启用特定的性能,这些性能只有在合约被进行时能力应用。这种模式通常实现了某种拜访限度,因而只有被容许的行为者(如管理员或所有者)才有必要的权限来触发断路器并进行合约。

这种模式能够应用的一些场景有。

  • 当发现一个 bug 时,进行合同的性能。
  • 在达到某个状态后进行某些合约的性能(常常与状态机模式一起应用)。
  • 在降级过程中进行合同的性能,因而内部行为者不能在降级过程中扭转合同的状态。
  • 在部署新版本后进行合同的废除版本。

当初让咱们来看看如何实现一个断路器,以进行 MySmartContract 的 incrementCounter 性能,所以计数器不会扭转在迁徙过程中。这个批改须要在 Version 1 中,也就是第一次部署的时候进行。

// Version 1 implementing a Circuit Breaker with access restriction to owner
contract MySmartContract {
    uint32 public counter;
    bool private stopped = false;
    address private owner;
    /**
    @dev Checks if the contract is not stopped; reverts if it is.
    */
    modifier isNotStopped {require(!stopped, 'Contract is stopped.');
        _;
    }
    /**
    @dev Enforces the caller to be the contract's owner.
    */
    modifier isOwner {require(msg.sender == owner, 'Sender is not owner.');
        _;
    }
    constructor() public {
        counter = 0;
        // Sets the contract's owner as the address that deployed the contract.
        owner = msg.sender;
    }
    /**
    @notice Increments the contract's counter if contract is active.
    @dev It will revert if the contract is stopped. See modifier "isNotStopped"
    */
    function incrementCounter() isNotStopped public {counter += 2; // This is an intentional bug.}
    /**
    @dev Stops / Unstops the contract.
    */
    function toggleContractStopped() isOwner public {stopped = !stopped;}
}

在下面的代码中,你能够看到 MySmartContract 的 Version 1 当初实现了一个修改器 isNotStopped。如果合同被进行,这个修饰符将复原交易。函数 incrementCounter 被批改为应用修饰符 isNotStopped,所以它将只在合约未进行时执行。通过这个实现,就在迁徙开始之前,合约的所有者能够调用函数 toggleContractStopped 并进行合约。请留神,这个函数应用修饰符 isOwner 来限度合同所有者的拜访。

要理解更多对于 Circuit Breakers 的信息,请务必查看 Consensys 对于 Circuit Breakers 的帖子和 OpenZeppelin 对 Pausable 合约的参考实现。

合同的数据(状态)迁徙

大多数智能合约须要在其外部存储中放弃某种状态。依据不同的用例,每个合约所须要的状态变量的数量有很大的不同。在咱们的例子中,原来 MySmartContract 的 Version 1 有一个繁多的状态变量计数器。当初思考 MySmartContract 的 Version 1 曾经应用了一段时间。当你发现 incrementCounter 函数的 bug 时,counter 的值曾经在 100 了。这种状况下会产生一些问题:

  • 你将如何解决 MySmartContract Version 2 的状态?
  • 你能够在 Version 2 中把计数器重置为 0(零),还是应该从 Version 1 中迁徙状态,以确保计数器在 Version 2 中初始化为 100?

这些问题的答案将取决于用例。在本文的例子中,这是一个非常简单的场景,而且 counter 没有重要的用法,如果将 counter 重置为 0,你不会有任何问题。然而,这不是大多数状况下所心愿的办法。假如你不能将值重置为 0,须要在第 2 版中将 counter 设置为 100。在 MySmartContract 这样一个简略的合约中,这并不艰难。你能够扭转 Version 2 的构造函数来接管计数器的初始值作为参数。在部署时,你会把值 100 传递给构造函数,这就解决了你的问题。实现这个办法后,MySmartContract Version 2 的构造函数会是这样的。

constructor(uint32 _counter) public {counter = _counter;}

如果你的用例像下面介绍的那样简略(或相似),从数据迁徙的角度来看,这可能是最合适的形式。实现其余办法的复杂性就不值得了。然而,请记住,大多数生产就绪的智能合约并不像 MySmartContract 那样简略,而且常常有更简单的状态。

当初思考一个应用多个构造、映射和数组的合约。如果你须要在具备如此简单存储的合约版本之间复制数据,你可能会面临以下一个或多个挑战:

  • 一堆交易要在区块链上解决,这可能须要相当长的工夫,这取决于数据集。
  • 用于解决从 ” 版本 1 ″ 读取数据并写入 ” 版本 2 ″ 的附加代码(除非手动实现)。
  • 花真金白银来领取 GAS。记住,你须要领取 GAS 来解决区块链中的交易。依据 Ethereum 黄皮书 – 附录 G. 费用表,用于向 Ethereum 写入数据的上位代码 SSTORE 操作,” 当存储值从零设置为非零时 “ 须要破费 20000 个天然气单位,” 当存储值的零度不变时 “ 须要破费 5000 个天然气单位。
  • 通过应用某种机制(如断路器)解冻 Version 1 的状态,以确保在迁徙过程中没有更多的数据附加到 Version 1 上。
  • 实现拜访限度机制,以防止内部各方(与迁徙无关)在迁徙期间调用版本 2 的性能。为了确保版本一的数据能够复制 / 迁徙到版本二,而不会在版本二中受到侵害和 / 或毁坏,须要这样做。

在状态较为简单的合约中,执行降级所需的工作相当重要,而且在区块链上复制数据会产生相当大的气老本。应用库和代理能够帮忙你开发更容易降级的智能合约。采纳这种办法,数据将被保留在一个存储状态但不承当任何逻辑的合约中(状态合约)。第二个合约或库实现了逻辑,但不承当状态(逻辑合约)。所以当发现逻辑中的 bug 时,只须要降级逻辑合约,而不必放心迁徙状态合约中存储的状态(见下文注)。

注:这种办法个别应用 Delegatecall。状态合约应用 delegatecall 调用逻辑合约中的函数。而后逻辑合约在状态合约的上下文中执行它的逻辑,也就是说 “ 存储、以后地址和余额依然参考调用合约,只是代码取自被调用的地址 ”。(来自下面提到的 Solidity 文档)。

让 MySmartContract 更容易降级

上面你能够看到,如果咱们实现本文中形容的变动,版本 1 和版本 2 会是什么样子。须要再次提及的是,思考到 MySmartContract 的简略性:状态变量和逻辑,其应用的策略是能够承受的。
首先,让咱们看看版本 1 的变动。

// Version 1 — Without Upgradable Mechanisms
contract MySmartContract {
    uint32 public counter;
    constructor() public {counter = 0;}
    function incrementCounter() public {counter += 2; // This "bug" is intentional.}
}

在上面的代码中,版本 1 实现了一个带有拜访限度机制的断路器,一旦合同被废除,所有者能够进行合同。

//Version 1 — With Deprecation Mechanism
contract MySmartContract {
    uint32 public counter;
    bool private stopped = false;
    address private owner;
    /**
    @dev Checks if the contract is not stopped; reverts if it is.
    */
    modifier isNotStopped {require(!stopped, 'Contract is stopped.');
        _;
    }
    /**
    @dev Enforces the caller to be the contract's owner.
    */
    modifier isOwner {require(msg.sender == owner, 'Sender is not owner.');
        _;
    }
    constructor() public {
        counter = 0;
        // Sets the contract's owner as the address that deployed the contract.
        owner = msg.sender;
    }
    /**
    @notice Increments the contract's counter if contract is active.
    @dev It will revert is the contract is stopped. See modifier "isNotStopped"
    */
    function incrementCounter() isNotStopped public {counter += 2; // This is an intentional bug.}
    /**
    @dev Stops / Unstops the contract.
    */
    function toggleContractStopped() isOwner public {stopped = !stopped;}
}

当初让咱们看看第二版会是怎么的。第二版 – 没有可降级的机制:

contract MySmartContract {
    uint32 public counter;
    constructor(uint32 _counter) public {counter = _counter;}
    function incrementCounter() public {counter++;}
}

在上面的代码中,第 2 版实现了与第 1 版雷同的断路器和拜访限度机制。此外,它实现了一个构造函数,容许在部署期间设置计数器的初始值。这个机制能够应用,它能够在降级时应用,从旧版本复制数据。
版本 2 – 具备简略的降级机制。

//Version 2 — With Simple Upgradable Mechanism
contract MySmartContract {
    uint32 public counter;
    bool private stopped = false;
    address private owner;
    /**
    @dev Checks if the contract is not stopped; reverts if it is.
    */
    modifier isNotStopped {require(!stopped, 'Contract is stopped.');
        _;
    }
    /**
    @dev Enforces the caller to be the contract's owner.
    */
    modifier isOwner {require(msg.sender == owner, 'Sender is not owner.');
        _;
    }
    constructor(uint32 _counter) public {
        counter = _counter; // Allows setting counter's initial value on deployment.
        // Sets the contract's owner as the address that deployed the contract.
        owner = msg.sender;
    }
    /**
    @notice Increments the contract's counter if contract is active.
    @dev It will revert is the contract is stopped. See modifier "isNotStopped"
    */
    function incrementCounter() isNotStopped public {counter++; // Fixes bug introduced in version 1.}
    /**
    @dev Stops / Unstops the contract.
    */
    function toggleContractStopped() isOwner public {stopped = !stopped;}
}

尽管上述变动实现了一些有助于智能合约降级的机制,但本文结尾所形容的第一个挑战 – 用户要参考新合约的地址,并不是这些简略的技术就能解决的。须要更高级的模式,比方 Proxies 和 Registry,或者应用 ENS 给你的合约注册一个用户敌对的名字,来防止所有用户降级参考第二版的新地址。

结束语

在 Ethereum 白皮书的 DAO 局部形容了可降级智能合约的原理,内容如下。

“ 尽管实践上代码是不可变的,但咱们能够很容易地绕过这一点,并通过将代码分块放在独自的合约中,并将调用哪些合约的地址存储在可批改的存储空间中,来实现事实上的可变性。*”

尽管这是能够实现的,但智能合约的降级是相当具备挑战性的。区块链的不可变性为智能合约的降级减少了更多的复杂性,因为它迫使你仔细分析智能合约的应用场景,理解可用的机制,而后决定哪些机制适宜你的合约,这样潜在的和可能的降级就会很顺利。

智能合约降级性是一个沉闷的钻研畛域。相干的模式、机制和最佳实际仍在一直探讨和倒退中。应用库和一些设计模式,如断路器、拜访限度、代理和注册表,能够帮忙你解决一些挑战。然而,在比较复杂的场景下,仅靠这些机制可能无奈解决所有问题,你可能须要思考更简单的模式,比方本文没有提到的 Eternal Storage。

你能够在这个 github 仓库中查看残缺的源代码,包含相干的单元测试(为了简略起见,本文没有提到)。

原文:https://levelup.gitconnected.com/introduction-to-ethereum-smart-contract-upgradability-with-solidity-789cc497c56f

正文完
 0