如果合约发起者想要更新升级合约代码,从而使得账户数据和其他东西继续留存,以太坊可以提供这种功能吗?且能否在实现该功能的同时不改变合约地址呢?还是每一次更新都要发布一个新地址呢?

是否存在“附加”机制呢?——为合约添加一些新的功能而不用全部重写。

原文地址

10 人回答 0

10 个回答
投票数
最旧发布
最近发布

回答发布于 2018-09-05 16:26:01

一旦合约部署到区块链中,那么它就是最终版本的且不能更改。 当然,如果某些参数被允许可通过原始代码进行更改的话,那么就可以修改这些参数。

更新合约的一种方法是使用版本控制系统。 例如,您可以拥有一个入口合约——该合约只是将所有调用转发到最新版本的合约,这由可更新地址参数定义的。 您还可以使用名称注册表,并将其更新为指向最新版本的合约。

另一种方法是将您的逻辑代码放在库中,然后通过Solidity中的库使用CALLCODE功能来调用位于指定的可更新地址上的代码。 这样,用户数据可在版本之间保持不变。 但这有局限性——逻辑合约的ABI必须保证能够随着时间的推移保持不变。

Homestead版本编注

从Homestead版本开始,就有一个DELEGATECALL操作码。 这允准你在维护msg.sender和所有存储的同时,将一些调用转发到单独的合约上。

例如,你可以拥有一个维护相同地址和存储的合约,但要将所有调用转发到存储在变量中的地址上:

contract Relay {
    address public currentVersion;
    address public owner;

    function Relay(address initAddr){
        currentVersion = initAddr;
        owner = msg.sender;
    }

    function update(address newAddress){
        if(msg.sender != owner) throw;
        currentVersion = newAddress;
    }

    function(){
        if(!currentVersion.delegatecall(msg.data)) throw;
    }
}

这是我以前用来演示数据/代码隔离的一个老方法

||
||

回答发布于 2018-09-05 16:26:00

其中一种方法是使用如下所述的合约系统:

  1. 合约“Register” - 将包含系统中所有合约的“名称-地址”对;
  2. 合约Backend
  3. 使用Backend的合约Frontend
  4. 部署Register并获取地址;
  5. 部署Backend并将Backend的地址注册到已部署的Register上;
  6. Register的地址硬编码到Backend的源码中。在任何Frontend合约调用Backend之前,你应该调用Register合约并获取Backend的实际地址。

然后你可以随时更新Backend合约——只需部署新的合约并在Register中重新注册。

调用外部合约:solidity.readthedocs.org...

同见讨论论坛: forum.ethereum.org...


更新(UPD):相同但(也许)更有效的方法

首次部署:

  1. 编写合约Register——用自身地址作为构造函数参数来部署其他合约;
  2. 编写所有其他合约 ——“可更新升级”的合约结合了需要Register合约地址的构造函数;
    • 也许合约应该可以被解除或者有销毁的方法
  3. 部署Register给出其构造函数数据——所有其他合约来自于步骤2。

升级:

  1. 使用Register的相同地址部署新版“可升级”合约;

    • 或者如果你的Register可以部署其他合约——可以给他
  2. 可选)禁用/终止旧版本的“可升级”合约;

  3. Register中注册新版“可升级”合约的地址。
||
||

回答发布于 2018-09-05 16:25:59

合约代码是不可变的,存储是可变的,但是你不能执行放入存储中的代码(至少目前是这样)。

对合约的错误修正

至于错误修正,常见的模式是——用代理或寻找一个合约来替代真实网关,一旦发生更改或错误修复,它将被替换。替换它也意味着丢失旧的存储内容。

保留存储

如果你希望能够在保留存储的同时升级代码,那么可以考虑将存储和逻辑分离开。有一个专用的存储合约,它接受来自可信地址的写入调用(例如逻辑合约)。所有重要的存储都应与此相关联。

自毁后访问存储

截至今天,即使在自毁的情况下也没有实现真正的修剪,但在未来这肯定是会实现的。已有几个EIP在讨论这个问题了。

即使实现了修剪,它也不应该在瞬间发生,你应该能够从最后一个状态读取存储。还计划使归档节点无限期地保留状态 ——仅仅通过判断区块链的增长,我们还不确定没有限制是否可行。

在同一地址上重新部署

简而言之:实际上这是不可能的。合约地址是由发送者和nonce值计算出来的。而nonce值是有顺序的,没有任何间隙,也不会有重复。

虽然在理论上,可以使用不同的随机数和地址来计算出相同的哈希值,但可能性很小。

||
||

回答发布于 2018-09-05 16:25:58

部署在区块链上的合约是不可变的,因此这意味着:

  • 已部署合约的地址和代码无法更改
  • 部署一个较新(甚至完全一样)的合约将创建一个新地址
  • 代码无法添加到已部署的合约中

>问题中提到:如果合约发起者想要更新升级合约代码,从而使得账户数据和其他东西继续留存,以太坊可以为实现该功能提供什么解决办法吗?

扩展合约C1的一种简单方法是确保C1具有返回其所有数据的函数/存取器。可以编写新的合约C2,它调用C1函数并执行额外的或修正过的逻辑。 (注意,如果C1和C2有foo,其中C1的foo是错误的并且C2的foo被修正过,那么无法办法禁止C1的foo被调用。)

正如@Alexander的回答中所描述的,可以使用注册表,以便其他DApp和合约在注册表中查询合约C的地址,这样当C1被C2“替换”时,不需要更改DApp代码。以这种方式使用注册表可以防止对C1的地址进行硬编码(以便C2,C3,C4可以在需要时取而代之),但DApp确实需要对注册表的地址进行硬编码。

编注:ENS,以太坊域名服务,刚刚被部署在测试网(Ropsten)。

有关快速入门和其他详细信息,请参阅ENS wiki。下面是一个介绍:

ENS是以太坊域名服务,一种基于以太坊区块链的分布式的、可扩展的命名系统。

ENS可用于解决各种资源。ENS的初始标准定义了以太坊地址的解析,但系统可以通过设计进行扩展,允许在将来解决更多资源类型,而无需ENS的核心组件进行升级。

ENS部署在Ropsten 测试网络上,地址为0x112234455c3a32fd11230c42e7bccd4a84e02010。

这里是最初的讨论

||
||

回答发布于 2018-09-05 16:25:56

@Nick Johnson有一个可升级合约的基础合约

正如他所说,在使用之前应该“完全了解它的局限和缺点”。

/**
 * Base contract that all upgradeable contracts should use.
 * 
 * Contracts implementing this interface are all called using delegatecall from
 * a dispatcher. As a result, the _sizes and _dest variables are shared with the
 * dispatcher contract, which allows the called contract to update these at will.
 * 
 * _sizes is a map of function signatures to return value sizes. Due to EVM
 * limitations, these need to be populated by the target contract, so the
 * dispatcher knows how many bytes of data to return from called functions.
 * Unfortunately, this makes variable-length return values impossible.
 * 
 * _dest is the address of the contract currently implementing all the
 * functionality of the composite contract. Contracts should update this by
 * calling the internal function `replace`, which updates _dest and calls
 * `initialize()` on the new contract.
 * 
 * When upgrading a contract, restrictions on permissible changes to the set of
 * storage variables must be observed. New variables may be added, but existing
 * ones may not be deleted or replaced. Changing variable names is acceptable.
 * Structs in arrays may not be modified, but structs in maps can be, following
 * the same rules described above.
 */
contract Upgradeable {
    mapping(bytes4=>uint32) _sizes;
    address _dest;

    /**
     * This function is called using delegatecall from the dispatcher when the
     * target contract is first initialized. It should use this opportunity to
     * insert any return data sizes in _sizes, and perform any other upgrades
     * necessary to change over from the old contract implementation (if any).
     * 
     * Implementers of this function should either perform strictly harmless,
     * idempotent operations like setting return sizes, or use some form of
     * access control, to prevent outside callers.
     */
    function initialize();

    /**
     * Performs a handover to a new implementing contract.
     */
    function replace(address target) internal {
        _dest = target;
        target.delegatecall(bytes4(sha3("initialize()")));
    }
}

/**
 * The dispatcher is a minimal 'shim' that dispatches calls to a targeted
 * contract. Calls are made using 'delegatecall', meaning all storage and value
 * is kept on the dispatcher. As a result, when the target is updated, the new
 * contract inherits all the stored data and value from the old contract.
 */
contract Dispatcher is Upgradeable {
    function Dispatcher(address target) {
        replace(target);
    }

    function initialize() {
        // Should only be called by on target contracts, not on the dispatcher
        throw;
    }

    function() {
        bytes4 sig;
        assembly { sig := calldataload(0) }
        var len = _sizes[sig];
        var target = _dest;

        assembly {
            // return _dest.delegatecall(msg.data)
            calldatacopy(0x0, 0x0, calldatasize)
            delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
            return(0, len)
        }
    }
}

contract Example is Upgradeable {
    uint _value;

    function initialize() {
        _sizes[bytes4(sha3("getUint()"))] = 32;
    }

    function getUint() returns (uint) {
        return _value;
    }

    function setUint(uint value) {
        _value = value;
    }
}
||
||

回答发布于 2018-09-05 16:25:54

以太坊的基本原则之一:即智能合约在部署之后就无法修改。

但是,如果考虑了以下因素,你仍然可以拥有可升级的智能合约

这必须从一开始就计划好。虽然最关键的是第4点,但是其他几点对于顺利地实现智能合约升级也同样至关重要。

因此,在设计智能合约时,你需要需要考虑以下5个要素:

  1. 保持智能合约模块化,将数据结构中的规则和逻辑完全完全分离开。因此,如果你需要更改某些内容,将只更改相关合约,而无需更改许多或全部合约内容。
  2. 应准备好紧急停止或断路器,以便能够在任何迁移过程中停止所有操作。因为你不希望处于下面这种境况:人们仍然可以在迁移过程中以及之后的这段时间内将数据更新/插入到旧版本的智能合约中。
  3. 事先应该能够读取智能合约中的所有数据。当然,你可以通过限授权所有者或任何其他可信用户,亦或甚至是另一个智能合约来限制数据读取。你需要从旧版智能合约中读取数据并插入到新版本中。
  4. 你将使用以下策略与智能合约进行通信。我从智能合约最佳实践(Smart Contact Best Practices)中复制了它们:

升级破损的合约

如果发现了错误或需要进行改进的地方,则需要更新代码。 发现错误虽然很好,但是我们没有办法解决它。

...

然而,有两种最常用的基本方法。 两者中较简单的是拥有一个注册合约,其中包含最新版本合约的地址。 对于合约用户更好的方法是生成一份合约——将调用和数据转发到最新版本的合约上。

示例1:使用注册表合约来存储最新版本的合约

在此示例中,不会转发调用,因此用户应当在每次与之交互之前获取当前地址。

contract SomeRegister {
    address backendContract;
    address[] previousBackends;
    address owner;

    function SomeRegister() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner)
        _;
    }

    function changeBackend(address newBackend) public
    onlyOwner()
    returns (bool)
    {
        if(newBackend != backendContract) {
            previousBackends.push(backendContract);
            backendContract = newBackend;
            return true;
        }

        return false;
    }
}

这种方法有两个主要的缺点:

  1. 用户必须始终查询当前地址,任何未能这样做的人都有可能使用旧版本的合约

  2. 在更换合约时,你需要仔细考虑如何处理合约中的数据

另一种方法是生成一份合约——将调用和数据转发到最新版本的合约上:

示例2:使用DELEGATECALL来转移数据和调用

contract Relay {
    address public currentVersion;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function Relay(address initAddr) {
        currentVersion = initAddr;
        owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
    }

    function changeContract(address newVersion) public
    onlyOwner()
    {
        currentVersion = newVersion;
    }

    function() {
        require(currentVersion.delegatecall(msg.data));
    }
}

这种方法虽然避免了以前的问题,但也存在新的问题。就如何在此合约中存储数据的问题,你必须谨慎考虑。如果你的新合约具有与第一个不同的存储布局,则你的数据可能最终会有所损坏。此外,这个简单版本的模式不能从返回函数值,只能转发它们——这限制了它的适用性。 (更复杂的实现尝试使用内联汇编代码和返回大小的注册表来解决此问题。)

无论你的方法如何,重要的是有一些方法可以升级你的合约,否则当发现不可避免的错误时,它们将会无法使用。

然而,我还建议检查由Zeppelin Solutions和Aragon发布的Solidity代理库(Proxy Libraries in Solidity)。有计划为此事制定行业标准。

  1. 你必须有一个很好的测试战略与战术。因为更新智能合约的代价真的会毁了你的全部。

我为此在Medium上创作了一个故事,标题为:以太坊dApps的基本设计考虑(1):可升级的智能合约以及我为上述每一点提供的示例。

||
||

回答发布于 2018-09-05 16:25:53

在参考了colony.io上关于可升级合约的帖子后,我们(我和我的团队)最近也正在研究可升级的合约问题。因此,我们拥有不同层面的合约而不是仅仅一个合约。

如果我简要地描述它,那么需要使存储部分非常通用,这样一旦创建它,你就可以存储各种类型的数据(借助setter方法)并访问它(借助getter方法) 。这使你的数据永久存储,将来不需要更改。

查看此数据存储合约以便更好地了解它 - https://goo.gl/aLmvJ5

第二层应该是包含函数的主合约——这部分可在之后更新并且为了使用旧的数据存储,你应该以这种方式创建合约以便将最新部署的合约指向已存在的数据存储,然后在新合约可以直接关联旧数据之后销毁旧合约。

查看我们的代码库,可以了解我们是如何实现可升级合约- https://goo.gl/p5zGEv

注意:在上面的GitHub repo中,由于我们的用例,所以使用了三层合约。但是,可以仅使用两层进行合约升级。

希望这可以给你帮助。

||
||

回答发布于 2018-09-05 16:25:52

允许你有一个地址不变、完全可控且可更新升级的合约。

https://github.com/u2/ether-router

https://github.com/ConsenSys/smart-contract-best-practices#upgrading-broken-contracts

||
||

回答发布于 2018-09-05 16:25:50

zos引入了一个框架可以为我们实现可更新升级的智能合约。

PTAL: https://docs.zeppelinos.org/docs/start.html

||
||

回答发布于 2018-09-05 16:25:49

可升级智能合约中的真正问题是如何从合约中迁移已存储的值。

构建可升级智能合约的更好方法是不同合约中的存储和逻辑区分开。

将所有合约数据保存在一个智能合约中——该合约仅接受来自逻辑合约的调用。

不断更新逻辑合约的逻辑。 但是,在定义存储合约的变量时,你需要想的长远一点。

||
||

回答发布于 2018-09-05 16:25:49

可升级智能合约中的真正问题是如何从合约中迁移已存储的值。

构建可升级智能合约的更好方法是不同合约中的存储和逻辑区分开。

将所有合约数据保存在一个智能合约中——该合约仅接受来自逻辑合约的调用。

不断更新逻辑合约的逻辑。 但是,在定义存储合约的变量时,你需要想的长远一点。

||
||

回答发布于 2018-09-05 16:25:50

zos引入了一个框架可以为我们实现可更新升级的智能合约。

PTAL: https://docs.zeppelinos.org/docs/start.html

||
||

回答发布于 2018-09-05 16:25:52

允许你有一个地址不变、完全可控且可更新升级的合约。

https://github.com/u2/ether-router

https://github.com/ConsenSys/smart-contract-best-practices#upgrading-broken-contracts

||
||

回答发布于 2018-09-05 16:25:53

在参考了colony.io上关于可升级合约的帖子后,我们(我和我的团队)最近也正在研究可升级的合约问题。因此,我们拥有不同层面的合约而不是仅仅一个合约。

如果我简要地描述它,那么需要使存储部分非常通用,这样一旦创建它,你就可以存储各种类型的数据(借助setter方法)并访问它(借助getter方法) 。这使你的数据永久存储,将来不需要更改。

查看此数据存储合约以便更好地了解它 - https://goo.gl/aLmvJ5

第二层应该是包含函数的主合约——这部分可在之后更新并且为了使用旧的数据存储,你应该以这种方式创建合约以便将最新部署的合约指向已存在的数据存储,然后在新合约可以直接关联旧数据之后销毁旧合约。

查看我们的代码库,可以了解我们是如何实现可升级合约- https://goo.gl/p5zGEv

注意:在上面的GitHub repo中,由于我们的用例,所以使用了三层合约。但是,可以仅使用两层进行合约升级。

希望这可以给你帮助。

||
||

回答发布于 2018-09-05 16:25:54

以太坊的基本原则之一:即智能合约在部署之后就无法修改。

但是,如果考虑了以下因素,你仍然可以拥有可升级的智能合约

这必须从一开始就计划好。虽然最关键的是第4点,但是其他几点对于顺利地实现智能合约升级也同样至关重要。

因此,在设计智能合约时,你需要需要考虑以下5个要素:

  1. 保持智能合约模块化,将数据结构中的规则和逻辑完全完全分离开。因此,如果你需要更改某些内容,将只更改相关合约,而无需更改许多或全部合约内容。
  2. 应准备好紧急停止或断路器,以便能够在任何迁移过程中停止所有操作。因为你不希望处于下面这种境况:人们仍然可以在迁移过程中以及之后的这段时间内将数据更新/插入到旧版本的智能合约中。
  3. 事先应该能够读取智能合约中的所有数据。当然,你可以通过限授权所有者或任何其他可信用户,亦或甚至是另一个智能合约来限制数据读取。你需要从旧版智能合约中读取数据并插入到新版本中。
  4. 你将使用以下策略与智能合约进行通信。我从智能合约最佳实践(Smart Contact Best Practices)中复制了它们:

升级破损的合约

如果发现了错误或需要进行改进的地方,则需要更新代码。 发现错误虽然很好,但是我们没有办法解决它。

...

然而,有两种最常用的基本方法。 两者中较简单的是拥有一个注册合约,其中包含最新版本合约的地址。 对于合约用户更好的方法是生成一份合约——将调用和数据转发到最新版本的合约上。

示例1:使用注册表合约来存储最新版本的合约

在此示例中,不会转发调用,因此用户应当在每次与之交互之前获取当前地址。

contract SomeRegister {
    address backendContract;
    address[] previousBackends;
    address owner;

    function SomeRegister() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner)
        _;
    }

    function changeBackend(address newBackend) public
    onlyOwner()
    returns (bool)
    {
        if(newBackend != backendContract) {
            previousBackends.push(backendContract);
            backendContract = newBackend;
            return true;
        }

        return false;
    }
}

这种方法有两个主要的缺点:

  1. 用户必须始终查询当前地址,任何未能这样做的人都有可能使用旧版本的合约

  2. 在更换合约时,你需要仔细考虑如何处理合约中的数据

另一种方法是生成一份合约——将调用和数据转发到最新版本的合约上:

示例2:使用DELEGATECALL来转移数据和调用

contract Relay {
    address public currentVersion;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function Relay(address initAddr) {
        currentVersion = initAddr;
        owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
    }

    function changeContract(address newVersion) public
    onlyOwner()
    {
        currentVersion = newVersion;
    }

    function() {
        require(currentVersion.delegatecall(msg.data));
    }
}

这种方法虽然避免了以前的问题,但也存在新的问题。就如何在此合约中存储数据的问题,你必须谨慎考虑。如果你的新合约具有与第一个不同的存储布局,则你的数据可能最终会有所损坏。此外,这个简单版本的模式不能从返回函数值,只能转发它们——这限制了它的适用性。 (更复杂的实现尝试使用内联汇编代码和返回大小的注册表来解决此问题。)

无论你的方法如何,重要的是有一些方法可以升级你的合约,否则当发现不可避免的错误时,它们将会无法使用。

然而,我还建议检查由Zeppelin Solutions和Aragon发布的Solidity代理库(Proxy Libraries in Solidity)。有计划为此事制定行业标准。

  1. 你必须有一个很好的测试战略与战术。因为更新智能合约的代价真的会毁了你的全部。

我为此在Medium上创作了一个故事,标题为:以太坊dApps的基本设计考虑(1):可升级的智能合约以及我为上述每一点提供的示例。

||
||

回答发布于 2018-09-05 16:25:56

@Nick Johnson有一个可升级合约的基础合约

正如他所说,在使用之前应该“完全了解它的局限和缺点”。

/**
 * Base contract that all upgradeable contracts should use.
 * 
 * Contracts implementing this interface are all called using delegatecall from
 * a dispatcher. As a result, the _sizes and _dest variables are shared with the
 * dispatcher contract, which allows the called contract to update these at will.
 * 
 * _sizes is a map of function signatures to return value sizes. Due to EVM
 * limitations, these need to be populated by the target contract, so the
 * dispatcher knows how many bytes of data to return from called functions.
 * Unfortunately, this makes variable-length return values impossible.
 * 
 * _dest is the address of the contract currently implementing all the
 * functionality of the composite contract. Contracts should update this by
 * calling the internal function `replace`, which updates _dest and calls
 * `initialize()` on the new contract.
 * 
 * When upgrading a contract, restrictions on permissible changes to the set of
 * storage variables must be observed. New variables may be added, but existing
 * ones may not be deleted or replaced. Changing variable names is acceptable.
 * Structs in arrays may not be modified, but structs in maps can be, following
 * the same rules described above.
 */
contract Upgradeable {
    mapping(bytes4=>uint32) _sizes;
    address _dest;

    /**
     * This function is called using delegatecall from the dispatcher when the
     * target contract is first initialized. It should use this opportunity to
     * insert any return data sizes in _sizes, and perform any other upgrades
     * necessary to change over from the old contract implementation (if any).
     * 
     * Implementers of this function should either perform strictly harmless,
     * idempotent operations like setting return sizes, or use some form of
     * access control, to prevent outside callers.
     */
    function initialize();

    /**
     * Performs a handover to a new implementing contract.
     */
    function replace(address target) internal {
        _dest = target;
        target.delegatecall(bytes4(sha3("initialize()")));
    }
}

/**
 * The dispatcher is a minimal 'shim' that dispatches calls to a targeted
 * contract. Calls are made using 'delegatecall', meaning all storage and value
 * is kept on the dispatcher. As a result, when the target is updated, the new
 * contract inherits all the stored data and value from the old contract.
 */
contract Dispatcher is Upgradeable {
    function Dispatcher(address target) {
        replace(target);
    }

    function initialize() {
        // Should only be called by on target contracts, not on the dispatcher
        throw;
    }

    function() {
        bytes4 sig;
        assembly { sig := calldataload(0) }
        var len = _sizes[sig];
        var target = _dest;

        assembly {
            // return _dest.delegatecall(msg.data)
            calldatacopy(0x0, 0x0, calldatasize)
            delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
            return(0, len)
        }
    }
}

contract Example is Upgradeable {
    uint _value;

    function initialize() {
        _sizes[bytes4(sha3("getUint()"))] = 32;
    }

    function getUint() returns (uint) {
        return _value;
    }

    function setUint(uint value) {
        _value = value;
    }
}
||
||

回答发布于 2018-09-05 16:25:58

部署在区块链上的合约是不可变的,因此这意味着:

  • 已部署合约的地址和代码无法更改
  • 部署一个较新(甚至完全一样)的合约将创建一个新地址
  • 代码无法添加到已部署的合约中

>问题中提到:如果合约发起者想要更新升级合约代码,从而使得账户数据和其他东西继续留存,以太坊可以为实现该功能提供什么解决办法吗?

扩展合约C1的一种简单方法是确保C1具有返回其所有数据的函数/存取器。可以编写新的合约C2,它调用C1函数并执行额外的或修正过的逻辑。 (注意,如果C1和C2有foo,其中C1的foo是错误的并且C2的foo被修正过,那么无法办法禁止C1的foo被调用。)

正如@Alexander的回答中所描述的,可以使用注册表,以便其他DApp和合约在注册表中查询合约C的地址,这样当C1被C2“替换”时,不需要更改DApp代码。以这种方式使用注册表可以防止对C1的地址进行硬编码(以便C2,C3,C4可以在需要时取而代之),但DApp确实需要对注册表的地址进行硬编码。

编注:ENS,以太坊域名服务,刚刚被部署在测试网(Ropsten)。

有关快速入门和其他详细信息,请参阅ENS wiki。下面是一个介绍:

ENS是以太坊域名服务,一种基于以太坊区块链的分布式的、可扩展的命名系统。

ENS可用于解决各种资源。ENS的初始标准定义了以太坊地址的解析,但系统可以通过设计进行扩展,允许在将来解决更多资源类型,而无需ENS的核心组件进行升级。

ENS部署在Ropsten 测试网络上,地址为0x112234455c3a32fd11230c42e7bccd4a84e02010。

这里是最初的讨论

||
||

回答发布于 2018-09-05 16:25:59

合约代码是不可变的,存储是可变的,但是你不能执行放入存储中的代码(至少目前是这样)。

对合约的错误修正

至于错误修正,常见的模式是——用代理或寻找一个合约来替代真实网关,一旦发生更改或错误修复,它将被替换。替换它也意味着丢失旧的存储内容。

保留存储

如果你希望能够在保留存储的同时升级代码,那么可以考虑将存储和逻辑分离开。有一个专用的存储合约,它接受来自可信地址的写入调用(例如逻辑合约)。所有重要的存储都应与此相关联。

自毁后访问存储

截至今天,即使在自毁的情况下也没有实现真正的修剪,但在未来这肯定是会实现的。已有几个EIP在讨论这个问题了。

即使实现了修剪,它也不应该在瞬间发生,你应该能够从最后一个状态读取存储。还计划使归档节点无限期地保留状态 ——仅仅通过判断区块链的增长,我们还不确定没有限制是否可行。

在同一地址上重新部署

简而言之:实际上这是不可能的。合约地址是由发送者和nonce值计算出来的。而nonce值是有顺序的,没有任何间隙,也不会有重复。

虽然在理论上,可以使用不同的随机数和地址来计算出相同的哈希值,但可能性很小。

||
||

回答发布于 2018-09-05 16:26:00

其中一种方法是使用如下所述的合约系统:

  1. 合约“Register” - 将包含系统中所有合约的“名称-地址”对;
  2. 合约Backend
  3. 使用Backend的合约Frontend
  4. 部署Register并获取地址;
  5. 部署Backend并将Backend的地址注册到已部署的Register上;
  6. Register的地址硬编码到Backend的源码中。在任何Frontend合约调用Backend之前,你应该调用Register合约并获取Backend的实际地址。

然后你可以随时更新Backend合约——只需部署新的合约并在Register中重新注册。

调用外部合约:solidity.readthedocs.org...

同见讨论论坛: forum.ethereum.org...


更新(UPD):相同但(也许)更有效的方法

首次部署:

  1. 编写合约Register——用自身地址作为构造函数参数来部署其他合约;
  2. 编写所有其他合约 ——“可更新升级”的合约结合了需要Register合约地址的构造函数;
    • 也许合约应该可以被解除或者有销毁的方法
  3. 部署Register给出其构造函数数据——所有其他合约来自于步骤2。

升级:

  1. 使用Register的相同地址部署新版“可升级”合约;

    • 或者如果你的Register可以部署其他合约——可以给他
  2. 可选)禁用/终止旧版本的“可升级”合约;

  3. Register中注册新版“可升级”合约的地址。
||
||

回答发布于 2018-09-05 16:26:01

一旦合约部署到区块链中,那么它就是最终版本的且不能更改。 当然,如果某些参数被允许可通过原始代码进行更改的话,那么就可以修改这些参数。

更新合约的一种方法是使用版本控制系统。 例如,您可以拥有一个入口合约——该合约只是将所有调用转发到最新版本的合约,这由可更新地址参数定义的。 您还可以使用名称注册表,并将其更新为指向最新版本的合约。

另一种方法是将您的逻辑代码放在库中,然后通过Solidity中的库使用CALLCODE功能来调用位于指定的可更新地址上的代码。 这样,用户数据可在版本之间保持不变。 但这有局限性——逻辑合约的ABI必须保证能够随着时间的推移保持不变。

Homestead版本编注

从Homestead版本开始,就有一个DELEGATECALL操作码。 这允准你在维护msg.sender和所有存储的同时,将一些调用转发到单独的合约上。

例如,你可以拥有一个维护相同地址和存储的合约,但要将所有调用转发到存储在变量中的地址上:

contract Relay {
    address public currentVersion;
    address public owner;

    function Relay(address initAddr){
        currentVersion = initAddr;
        owner = msg.sender;
    }

    function update(address newAddress){
        if(msg.sender != owner) throw;
        currentVersion = newAddress;
    }

    function(){
        if(!currentVersion.delegatecall(msg.data)) throw;
    }
}

这是我以前用来演示数据/代码隔离的一个老方法

||
||

回答发布于 2018-09-05 16:26:01

一旦合约部署到区块链中,那么它就是最终版本的且不能更改。 当然,如果某些参数被允许可通过原始代码进行更改的话,那么就可以修改这些参数。

更新合约的一种方法是使用版本控制系统。 例如,您可以拥有一个入口合约——该合约只是将所有调用转发到最新版本的合约,这由可更新地址参数定义的。 您还可以使用名称注册表,并将其更新为指向最新版本的合约。

另一种方法是将您的逻辑代码放在库中,然后通过Solidity中的库使用CALLCODE功能来调用位于指定的可更新地址上的代码。 这样,用户数据可在版本之间保持不变。 但这有局限性——逻辑合约的ABI必须保证能够随着时间的推移保持不变。

Homestead版本编注

从Homestead版本开始,就有一个DELEGATECALL操作码。 这允准你在维护msg.sender和所有存储的同时,将一些调用转发到单独的合约上。

例如,你可以拥有一个维护相同地址和存储的合约,但要将所有调用转发到存储在变量中的地址上:

contract Relay {
    address public currentVersion;
    address public owner;

    function Relay(address initAddr){
        currentVersion = initAddr;
        owner = msg.sender;
    }

    function update(address newAddress){
        if(msg.sender != owner) throw;
        currentVersion = newAddress;
    }

    function(){
        if(!currentVersion.delegatecall(msg.data)) throw;
    }
}

这是我以前用来演示数据/代码隔离的一个老方法

||
||

回答发布于 2018-09-05 16:26:00

其中一种方法是使用如下所述的合约系统:

  1. 合约“Register” - 将包含系统中所有合约的“名称-地址”对;
  2. 合约Backend
  3. 使用Backend的合约Frontend
  4. 部署Register并获取地址;
  5. 部署Backend并将Backend的地址注册到已部署的Register上;
  6. Register的地址硬编码到Backend的源码中。在任何Frontend合约调用Backend之前,你应该调用Register合约并获取Backend的实际地址。

然后你可以随时更新Backend合约——只需部署新的合约并在Register中重新注册。

调用外部合约:solidity.readthedocs.org...

同见讨论论坛: forum.ethereum.org...


更新(UPD):相同但(也许)更有效的方法

首次部署:

  1. 编写合约Register——用自身地址作为构造函数参数来部署其他合约;
  2. 编写所有其他合约 ——“可更新升级”的合约结合了需要Register合约地址的构造函数;
    • 也许合约应该可以被解除或者有销毁的方法
  3. 部署Register给出其构造函数数据——所有其他合约来自于步骤2。

升级:

  1. 使用Register的相同地址部署新版“可升级”合约;

    • 或者如果你的Register可以部署其他合约——可以给他
  2. 可选)禁用/终止旧版本的“可升级”合约;

  3. Register中注册新版“可升级”合约的地址。
||
||

回答发布于 2018-09-05 16:25:59

合约代码是不可变的,存储是可变的,但是你不能执行放入存储中的代码(至少目前是这样)。

对合约的错误修正

至于错误修正,常见的模式是——用代理或寻找一个合约来替代真实网关,一旦发生更改或错误修复,它将被替换。替换它也意味着丢失旧的存储内容。

保留存储

如果你希望能够在保留存储的同时升级代码,那么可以考虑将存储和逻辑分离开。有一个专用的存储合约,它接受来自可信地址的写入调用(例如逻辑合约)。所有重要的存储都应与此相关联。

自毁后访问存储

截至今天,即使在自毁的情况下也没有实现真正的修剪,但在未来这肯定是会实现的。已有几个EIP在讨论这个问题了。

即使实现了修剪,它也不应该在瞬间发生,你应该能够从最后一个状态读取存储。还计划使归档节点无限期地保留状态 ——仅仅通过判断区块链的增长,我们还不确定没有限制是否可行。

在同一地址上重新部署

简而言之:实际上这是不可能的。合约地址是由发送者和nonce值计算出来的。而nonce值是有顺序的,没有任何间隙,也不会有重复。

虽然在理论上,可以使用不同的随机数和地址来计算出相同的哈希值,但可能性很小。

||
||

回答发布于 2018-09-05 16:25:58

部署在区块链上的合约是不可变的,因此这意味着:

  • 已部署合约的地址和代码无法更改
  • 部署一个较新(甚至完全一样)的合约将创建一个新地址
  • 代码无法添加到已部署的合约中

>问题中提到:如果合约发起者想要更新升级合约代码,从而使得账户数据和其他东西继续留存,以太坊可以为实现该功能提供什么解决办法吗?

扩展合约C1的一种简单方法是确保C1具有返回其所有数据的函数/存取器。可以编写新的合约C2,它调用C1函数并执行额外的或修正过的逻辑。 (注意,如果C1和C2有foo,其中C1的foo是错误的并且C2的foo被修正过,那么无法办法禁止C1的foo被调用。)

正如@Alexander的回答中所描述的,可以使用注册表,以便其他DApp和合约在注册表中查询合约C的地址,这样当C1被C2“替换”时,不需要更改DApp代码。以这种方式使用注册表可以防止对C1的地址进行硬编码(以便C2,C3,C4可以在需要时取而代之),但DApp确实需要对注册表的地址进行硬编码。

编注:ENS,以太坊域名服务,刚刚被部署在测试网(Ropsten)。

有关快速入门和其他详细信息,请参阅ENS wiki。下面是一个介绍:

ENS是以太坊域名服务,一种基于以太坊区块链的分布式的、可扩展的命名系统。

ENS可用于解决各种资源。ENS的初始标准定义了以太坊地址的解析,但系统可以通过设计进行扩展,允许在将来解决更多资源类型,而无需ENS的核心组件进行升级。

ENS部署在Ropsten 测试网络上,地址为0x112234455c3a32fd11230c42e7bccd4a84e02010。

这里是最初的讨论

||
||

回答发布于 2018-09-05 16:25:56

@Nick Johnson有一个可升级合约的基础合约

正如他所说,在使用之前应该“完全了解它的局限和缺点”。

/**
 * Base contract that all upgradeable contracts should use.
 * 
 * Contracts implementing this interface are all called using delegatecall from
 * a dispatcher. As a result, the _sizes and _dest variables are shared with the
 * dispatcher contract, which allows the called contract to update these at will.
 * 
 * _sizes is a map of function signatures to return value sizes. Due to EVM
 * limitations, these need to be populated by the target contract, so the
 * dispatcher knows how many bytes of data to return from called functions.
 * Unfortunately, this makes variable-length return values impossible.
 * 
 * _dest is the address of the contract currently implementing all the
 * functionality of the composite contract. Contracts should update this by
 * calling the internal function `replace`, which updates _dest and calls
 * `initialize()` on the new contract.
 * 
 * When upgrading a contract, restrictions on permissible changes to the set of
 * storage variables must be observed. New variables may be added, but existing
 * ones may not be deleted or replaced. Changing variable names is acceptable.
 * Structs in arrays may not be modified, but structs in maps can be, following
 * the same rules described above.
 */
contract Upgradeable {
    mapping(bytes4=>uint32) _sizes;
    address _dest;

    /**
     * This function is called using delegatecall from the dispatcher when the
     * target contract is first initialized. It should use this opportunity to
     * insert any return data sizes in _sizes, and perform any other upgrades
     * necessary to change over from the old contract implementation (if any).
     * 
     * Implementers of this function should either perform strictly harmless,
     * idempotent operations like setting return sizes, or use some form of
     * access control, to prevent outside callers.
     */
    function initialize();

    /**
     * Performs a handover to a new implementing contract.
     */
    function replace(address target) internal {
        _dest = target;
        target.delegatecall(bytes4(sha3("initialize()")));
    }
}

/**
 * The dispatcher is a minimal 'shim' that dispatches calls to a targeted
 * contract. Calls are made using 'delegatecall', meaning all storage and value
 * is kept on the dispatcher. As a result, when the target is updated, the new
 * contract inherits all the stored data and value from the old contract.
 */
contract Dispatcher is Upgradeable {
    function Dispatcher(address target) {
        replace(target);
    }

    function initialize() {
        // Should only be called by on target contracts, not on the dispatcher
        throw;
    }

    function() {
        bytes4 sig;
        assembly { sig := calldataload(0) }
        var len = _sizes[sig];
        var target = _dest;

        assembly {
            // return _dest.delegatecall(msg.data)
            calldatacopy(0x0, 0x0, calldatasize)
            delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
            return(0, len)
        }
    }
}

contract Example is Upgradeable {
    uint _value;

    function initialize() {
        _sizes[bytes4(sha3("getUint()"))] = 32;
    }

    function getUint() returns (uint) {
        return _value;
    }

    function setUint(uint value) {
        _value = value;
    }
}
||
||

回答发布于 2018-09-05 16:25:54

以太坊的基本原则之一:即智能合约在部署之后就无法修改。

但是,如果考虑了以下因素,你仍然可以拥有可升级的智能合约

这必须从一开始就计划好。虽然最关键的是第4点,但是其他几点对于顺利地实现智能合约升级也同样至关重要。

因此,在设计智能合约时,你需要需要考虑以下5个要素:

  1. 保持智能合约模块化,将数据结构中的规则和逻辑完全完全分离开。因此,如果你需要更改某些内容,将只更改相关合约,而无需更改许多或全部合约内容。
  2. 应准备好紧急停止或断路器,以便能够在任何迁移过程中停止所有操作。因为你不希望处于下面这种境况:人们仍然可以在迁移过程中以及之后的这段时间内将数据更新/插入到旧版本的智能合约中。
  3. 事先应该能够读取智能合约中的所有数据。当然,你可以通过限授权所有者或任何其他可信用户,亦或甚至是另一个智能合约来限制数据读取。你需要从旧版智能合约中读取数据并插入到新版本中。
  4. 你将使用以下策略与智能合约进行通信。我从智能合约最佳实践(Smart Contact Best Practices)中复制了它们:

升级破损的合约

如果发现了错误或需要进行改进的地方,则需要更新代码。 发现错误虽然很好,但是我们没有办法解决它。

...

然而,有两种最常用的基本方法。 两者中较简单的是拥有一个注册合约,其中包含最新版本合约的地址。 对于合约用户更好的方法是生成一份合约——将调用和数据转发到最新版本的合约上。

示例1:使用注册表合约来存储最新版本的合约

在此示例中,不会转发调用,因此用户应当在每次与之交互之前获取当前地址。

contract SomeRegister {
    address backendContract;
    address[] previousBackends;
    address owner;

    function SomeRegister() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner)
        _;
    }

    function changeBackend(address newBackend) public
    onlyOwner()
    returns (bool)
    {
        if(newBackend != backendContract) {
            previousBackends.push(backendContract);
            backendContract = newBackend;
            return true;
        }

        return false;
    }
}

这种方法有两个主要的缺点:

  1. 用户必须始终查询当前地址,任何未能这样做的人都有可能使用旧版本的合约

  2. 在更换合约时,你需要仔细考虑如何处理合约中的数据

另一种方法是生成一份合约——将调用和数据转发到最新版本的合约上:

示例2:使用DELEGATECALL来转移数据和调用

contract Relay {
    address public currentVersion;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function Relay(address initAddr) {
        currentVersion = initAddr;
        owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
    }

    function changeContract(address newVersion) public
    onlyOwner()
    {
        currentVersion = newVersion;
    }

    function() {
        require(currentVersion.delegatecall(msg.data));
    }
}

这种方法虽然避免了以前的问题,但也存在新的问题。就如何在此合约中存储数据的问题,你必须谨慎考虑。如果你的新合约具有与第一个不同的存储布局,则你的数据可能最终会有所损坏。此外,这个简单版本的模式不能从返回函数值,只能转发它们——这限制了它的适用性。 (更复杂的实现尝试使用内联汇编代码和返回大小的注册表来解决此问题。)

无论你的方法如何,重要的是有一些方法可以升级你的合约,否则当发现不可避免的错误时,它们将会无法使用。

然而,我还建议检查由Zeppelin Solutions和Aragon发布的Solidity代理库(Proxy Libraries in Solidity)。有计划为此事制定行业标准。

  1. 你必须有一个很好的测试战略与战术。因为更新智能合约的代价真的会毁了你的全部。

我为此在Medium上创作了一个故事,标题为:以太坊dApps的基本设计考虑(1):可升级的智能合约以及我为上述每一点提供的示例。

||
||

回答发布于 2018-09-05 16:25:53

在参考了colony.io上关于可升级合约的帖子后,我们(我和我的团队)最近也正在研究可升级的合约问题。因此,我们拥有不同层面的合约而不是仅仅一个合约。

如果我简要地描述它,那么需要使存储部分非常通用,这样一旦创建它,你就可以存储各种类型的数据(借助setter方法)并访问它(借助getter方法) 。这使你的数据永久存储,将来不需要更改。

查看此数据存储合约以便更好地了解它 - https://goo.gl/aLmvJ5

第二层应该是包含函数的主合约——这部分可在之后更新并且为了使用旧的数据存储,你应该以这种方式创建合约以便将最新部署的合约指向已存在的数据存储,然后在新合约可以直接关联旧数据之后销毁旧合约。

查看我们的代码库,可以了解我们是如何实现可升级合约- https://goo.gl/p5zGEv

注意:在上面的GitHub repo中,由于我们的用例,所以使用了三层合约。但是,可以仅使用两层进行合约升级。

希望这可以给你帮助。

||
||

回答发布于 2018-09-05 16:25:52

允许你有一个地址不变、完全可控且可更新升级的合约。

https://github.com/u2/ether-router

https://github.com/ConsenSys/smart-contract-best-practices#upgrading-broken-contracts

||
||

回答发布于 2018-09-05 16:25:50

zos引入了一个框架可以为我们实现可更新升级的智能合约。

PTAL: https://docs.zeppelinos.org/docs/start.html

||
||

回答发布于 2018-09-05 16:25:49

可升级智能合约中的真正问题是如何从合约中迁移已存储的值。

构建可升级智能合约的更好方法是不同合约中的存储和逻辑区分开。

将所有合约数据保存在一个智能合约中——该合约仅接受来自逻辑合约的调用。

不断更新逻辑合约的逻辑。 但是,在定义存储合约的变量时,你需要想的长远一点。

||
||