diff --git a/application/20230809-staged-contract-updates-resources/bigger_dependency_graph.png b/application/20230809-staged-contract-updates-resources/bigger_dependency_graph.png new file mode 100644 index 00000000..ec31fa5b Binary files /dev/null and b/application/20230809-staged-contract-updates-resources/bigger_dependency_graph.png differ diff --git a/application/20230809-staged-contract-updates-resources/interface_overview.png b/application/20230809-staged-contract-updates-resources/interface_overview.png new file mode 100644 index 00000000..b71cc50c Binary files /dev/null and b/application/20230809-staged-contract-updates-resources/interface_overview.png differ diff --git a/application/20230809-staged-contract-updates-resources/simple_dependency_graph.png b/application/20230809-staged-contract-updates-resources/simple_dependency_graph.png new file mode 100644 index 00000000..8d58e680 Binary files /dev/null and b/application/20230809-staged-contract-updates-resources/simple_dependency_graph.png differ diff --git a/application/20230809-staged-contract-updates.md b/application/20230809-staged-contract-updates.md new file mode 100644 index 00000000..41008284 --- /dev/null +++ b/application/20230809-staged-contract-updates.md @@ -0,0 +1,460 @@ +--- +status: accepted +flip: [179](https://github.com/onflow/flips/pull/179) +authors: Giovanni Sanchez (giovanni.sanchez@dapperlabs.com) +sponsor: Giovanni Sanchez (giovanni.sanchez@dapperlabs.com) +updated: 2023-09-26 +--- + +# FLIP 179: Staged Contract Update Mechanism + +## Objective + +This proposal outlines a mechanism for automated and efficient contract updates to take effect at or beyond a specified +block height with the goal of minimizing recovery time following breaking improvements. Included in this FLIP is a +design enabling contract developers to pre-define a sequence of contract updates across an arbitrary number of contracts +and accounts, and either execute these updates themselves or delegate deployment authority to some trusted party. + +## Motivation + +Immediately following planned Cadence improvements on Mainnet (marked by the Cadence 1.0 milestone), many contracts +across the ecosystem will need to be updated. In preparation for this milestone, the Flow community is considering ways +to make this migration as quick and seamless as possible, primarily motivated by the desire to minimize any +user-perceived downtime. With this in mind, it's in the interest of everyone to provide helpful and reliable tools to +empower and support developers in upgrading their contracts. + +Surely, some developers will prefer to manually execute updates themselves. However, others may find it useful and +even preferable to both codify their updated deployment using onchain constructs and additionally delegate update +execution to an automated service run by Flow as a part of the ecosystem-wide post-spork migration. + +## User Benefit + +This contract would benefit developers by making it easy for them to stage and even automate contract updates after +breaking improvements and benefit users by minimizing perceived downtime. + +## Design Proposal + +### Context + +Updates to a network of dependent contracts must occur according to their dependency ordering. Even when properly +ordered, any dependent contracts must wait for their dependencies to complete state changes in the execution environment +before they can successfully update. Said state change can only occur after the dependency updating transaction +completes. + +This is an important consideration in designing a wide-scale update mechanism because it means we must batch updates in +stages, with each stage updating a set of contracts at the same level of the dependency graph. + +If we were to take the following set of contracts with the pictured dependency graph and attempt to update them in a +single transaction, the attempt would fail. This is because `B` depends on `A`, and `A`'s updates won't yet take effect +until after the transaction has successfully executed. + +![Simple dependency graph](./20230809-staged-contract-updates-resources/simple_dependency_graph.png) + +*Sequencing a set of contracts into discrete update transactions, order labeled on the right.* + +Instead, we update `A` in one transaction, `B` in the following, and lastly `C` in a final transaction. Given a larger +number of contracts, we could update multiple contracts in a single transaction, but the updates would need to be +batched with contracts at a similar maximum depth in their dependency graph. + +![Simple dependency graph](./20230809-staged-contract-updates-resources/bigger_dependency_graph.png) + +Take for instance the graph above. We would stage `[A, D]` in the first transaction, `[B, E]` in another, and finally +`C` could be updated. Keep this mental model in mind as we put together this staged approach in the design below. + +### Overview + +> :information_source: The proposed design is largely informed by a working prototype which can be found in [this +> repo](https://github.com/sisyphusSmiling/contract-updater) + +Proposed is a single contract implementation defining a primary resource - referred to here as `Updater` - encapsulating +a bundle of ordered contract updates, themselves bundled more granularly into stages. A developer provides AuthAccount +Capabilities on the contract hosting accounts, staged and ordered update configuration details (contract address, name, +and code) to this resource on initialization as well as a blockheight boundary, at or beyond which updates should be +deployed. + +The owner of this resource can then call `Updater.update()` to execute the updates bundled into the current deployment +stage. The current stage is incremented every time `update()` is called, with `updateComplete` set to true once all +deployment stages have been attempted. + +In addition to the `Updater`, the `Delegatee` can capture delegated `Updater` Capabilities. This allows a developer to +codify their deployment details in an `Updater`, save the resource in their account, and offload the entirety of their +contract updates to a trusted party via `Delegatee`. + +With respect to Stable Cadence support, the idea is to provide a Flow-hosted `Delegatee` that executes delegated updates +(after updating core contracts) following the spork upgrade. At migration time, `Delegatee` can execute batches of +delegated updates, with each individual `Updater` tracking the current stage and deploying stage-specific contract +updates as configured by the resource owner. Since `Updater`s track their own stage, `Delegatee` simply needs to track +which `Updater` resources have remaining deployment stages and either execute those updates until all delegated updates +are complete or up to a maximum number of supported stages (preventing trolling attacks). + +### Implementation Details + + + +#### Interfaces + +![ContractUpdater interface overview](./20230809-staged-contract-updates-resources/interface_overview.png) + +*The overview above outlines the interface of `ContractUpdater` and each of its defined constructs. Expand the toggles +below to view each component in more detail.* + +
+struct ContractUpdate + +```cadence +/// Representative of a single contract, its name, code and where it will be deployed +/// +access(all) struct ContractUpdate { + access(all) let address: Address + access(all) let name: String + access(all) let code: String + access(all) fun toString(): String + access(all) fun codeAsCadence(): String +} +``` +
+ +
+resource Updater + +```cadence +/// Public interface enabling queries about the Updater +/// +access(all) resource interface UpdaterPublic { + access(all) fun getID(): UInt64 + access(all) fun getBlockUpdateBoundary(): UInt64 + access(all) fun getContractAccountAddresses(): [Address] + access(all) fun getDeployments(): [[ContractUpdate]] + access(all) fun getCurrentDeploymentStage(): Int + access(all) fun getFailedDeployments(): {Int: [String]} + access(all) fun hasBeenUpdated(): Bool +} + +/// Resource that enables delayed contract updates to wrapped accounts at or beyond a specified block height +/// +access(all) resource Updater : UpdaterPublic { + /// Update to occur at or beyond this block height + // TODO: Consider making this a contract-owned value as it's reflective of the spork height + access(self) let blockUpdateBoundary: UInt64 + /// Update status for each contract + access(self) var updateComplete: Bool + /// Capabilities for contract hosting accounts + access(self) let accounts: {Address: Capability} + /// Updates ordered by their deployment sequence and staged by their dependency depth + /// NOTE: Dev should be careful to validate their dependencies such that updates are performed from root + /// to leaf dependencies + access(self) let deployments: [[ContractUpdate]] + /// Current deployment stage + access(self) var currentDeploymentStage: Int + /// Contracts whose update failed keyed on their deployment stage + access(self) let failedDeployments: {Int: [String]} + + /// Executes the next update stage for all contracts defined in deployment, returning true if all stages have + /// been attempted and false if stages remain + /// + access(all) fun update(): Bool + + /* --- Public getters --- */ + // + access(all) fun getID(): UInt64 + access(all) fun getBlockUpdateBoundary(): UInt64 + access(all) fun getContractAccountAddresses(): [Address] + access(all) fun getDeployments(): [[ContractUpdate]] + access(all) fun getCurrentDeploymentStage(): Int + access(all) fun getFailedDeployments(): {Int: [String]} + access(all) fun hasBeenUpdated(): Bool +} +``` +
+ + +
+resource Delegatee + +```cadence +/// Public interface for Delegatee +/// +access(all) resource interface DelegateePublic { + access(all) fun check(id: UInt64): Bool? + access(all) fun getUpdaterIDs(): [UInt64] + access(all) fun delegate(updaterCap: Capability<&Updater>) + access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) +} + +/// Resource that executed delegated updates +/// +access(all) resource Delegatee : DelegateePublic { + // TODO: Block Height - All DelegatedUpdaters must be updated at or beyond this block height + // access(self) let blockUpdateBoundary: UInt64 + /// Track all delegated updaters + access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>} + + /// Checks if the specified DelegatedUpdater Capability is contained and valid + /// + access(all) fun check(id: UInt64): Bool? + /// Returns the IDs of the delegated updaters + /// + access(all) fun getUpdaterIDs(): [UInt64] + /// Allows for the delegation of updates to a contract + /// + access(all) fun delegate(updaterCap: Capability<&Updater>) + /// Enables Updaters to remove their delegation + /// + access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) + /// Executes update on the specified Updater, removing the Capability once update is completed + /// + access(all) fun update(updaterIDs: [UInt64]): [UInt64] + /// Enables admin removal of a DelegatedUpdater Capability + /// + access(all) fun removeDelegatedUpdater(id: UInt64) +} +``` +
+ + +
+Events + +```cadence +access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) +access(all) event UpdaterUpdated( + updaterUUID: UInt64, + updaterAddress: Address?, + blockUpdateBoundary: UInt64, + updatedAddresses: [Address], + updatedContracts: [String], + failedAddresses: [Address], + failedContracts: [String], + updateComplete: Bool +) +access(all) event UpdaterDelegationChanged(updaterUUID: UInt64, updaterAddress: Address?, delegated: Bool) +``` +
+ +
+Helper Methods + +```cadence +/// Returns the Address of the Delegatee associated with this contract +/// +access(all) fun getContractDelegateeAddress(): Address + +/// Helper method that returns the ordered array reflecting sequenced and staged deployments, with each contract +/// update represented by a ContractUpdate struct. +/// +/// NOTE: deploymentConfig is ordered, and the order is used to determine both the order of the contracts in each +/// deployment and the order of the deployments themselves. Each entry in the inner array must be exactly one +/// key-value pair, where the key is the address of the associated contract name and code. +/// +access(all) fun getDeploymentFromConfig(_ deploymentConfig: [[{Address: {String: String}}]]): [[ContractUpdate]] + +/// Returns a new Updater resource +/// +access(all) fun createNewUpdater( + blockUpdateBoundary: UInt64, + accounts: [Capability], + deployments: [[ContractUpdate]] +): @Updater + +/// Creates a new Delegatee resource enabling caller to self-host their Delegatee +/// +access(all) fun createNewDelegatee(): @Delegatee +``` +
+ + +#### Note on Update API + +> :information_source: The update API discussed below has been implemented - see PR: +> [onflow/cadence#2769](https://github.com/onflow/cadence/pull/2769) + +Currently, [updating a contract](https://developers.flow.com/cadence/language/contracts#updating-a-deployed-contract) +occurs via the API `Contracts.update__experimental(name: String, code: [UInt8]): DeployedContract` which reverts when an +update fails. Reverting on failure is perfectly fine for individual updates; however, doing so in the context of +iterative updates would prevent successive updates from executing, leading to a fragile design with cumbersome +alternatives. + +This design is heavily dependent on the existence of an alternative update API that avoids reverting in the event of a +failed update. Without it, each update would need to be executed in discrete transactions, resulting in at least an +order of magnitude more transaction executions to achieve the same result as this design, as well as more complex signing +architecture to handle the requisite number of proposal keys for all those transactions. + +The proposed `tryUpdate()` API is its own issue (found [here](https://github.com/onflow/cadence/issues/2700)), but is +included below for context and consideration: + +```cadence +access(all) enum ErrorType: UInt8 { + access(all) case CONTRACT_NOT_FOUND + access(all) case MULTIPLE_CONTRACTS_DECLARED + access(all) case MISMATCHED_NAME + access(all) case UNDEFINED +} + +access(all) struct DeploymentError { + access(all) let errorType: ErrorType + access(all) let errorMessage: String +} + +access(all) struct DeploymentResult { + access(all) let success: Bool + access(all) let errorMessage: DeploymentError? +} + +access(all) fun tryUpdate(name: String, code: [UInt8]): DeploymentResult +``` + +Ideally this method would return some error message in the event of a failed update; however, +[@turbolent](https://github.com/turbolent) mentioned security implications involved with exposing such error messages +into the execution environment without reverting. + +The proposed path forward then is to return the following `DeploymentResult`, later adding `errorMessage` once the +security implication has been mitigated: + +```cadence +access(all) struct DeploymentResult { + access(all) let success: Bool +} +``` + +With respect to the topic of this FLIP, the `Updater` can check `DeploymentResult.success` and emit any failed updates +within the executed stage as well as preserve the failure status within its fields. Direct follow up and intervention by +the `Updater` owner would then be to correct the issue and manually execute the contract updates. + +### Considerations + +#### Self-Defined Deployments + +Update executions via `Delegatee` can't account for the global dependency graph since ultimately contract account owners +have sovereignty over their update path. Therefore, `Delegatee` depends on the `Updater`'s creator to sequence the +contained deployment appropriately given the contracts to be updated. This means that developers will still want to +validate their contracts are both individually Stable Cadence compatible and updatable, as well as sequenced correctly +within the `Updater` resource. + +#### Handling Unsuccessful Updates + +Since we intend to iterate over a large number of updates per transaction, we cannot allow unsuccessful update attempts +to interrupt that iteration. Therefore, this mechanism provides no guarantees to the delegator that their updates will +be completed successfully or that any stages succeeding a failed contract update will be avoided. The only strong +guarantee to delegating parties is that the update deployment encapsulated in their `Updater` will be attempted at or +beyond the collective block height boundary. + +#### Examining Performance Constraints + +One significant callout deserving more investigation are the limitations presented by execution limits on both the +construction of the `Updater` resource as well as the execution of delegated updates. The former is a concern because it +would influence the `Updater` setup while the latter is a concern for update batch size executed by `Delegatee`. The +hope is that all delegated updates can be executed within a dozen or so transactions post-spork (based on the current +number of mainnet contracts), but benchmarks are needed to determine a well-informed and robust migration protocol. + +#### Supporting Resources + +Another drawback is the complexity associated with configuring and managing the `Updater` resource. This complexity is +three-fold: + +1. Configuring the `Updater` on initialization requires a fully sorted deployment complete with ordered stages, for + which there is currently non-existent support. +1. Once configured, there is currently no way to test whether the `Updater` will execute all updates successfully. +1. Once updates are executed, an `Updater`'s owner will want to know whether their update deployment completed + successfully. + +On 1/, it would be helpful to introduce a tool that would output a suggested `Updater` deployment ordering given a set +of Mainnet contract addresses & names as well as a tool to configure a local Flow project into the proper Cadence +json arguments for easy CLI configuration. + +On 2/, planned Stable Cadence emulator previews along with local Mainnet mirroring can fill the gaps here. The workflow +might look like: Mirror mainnet locally with the Stable Cadence preview running, configure your `Updater` based on your +updated contracts, then simulate the update execution locally. Guides and best practices on this front should follow if +this FLIP is approved and implemented. + +Regarding 3/, a helpful tool here would be an easy to use dashboard where the `Updater` owner can authenticate and view +both the current status of their `Updater` resource and any related events and transaction details where their updates +were executed. This would make it easy for developers to identify issues with their update deployments and +minimize follow up time. + +These gaps are building opportunities beyond the scope of this design, and contributions on these fronts are very +welcome. + +### Drawbacks + +Since we cannot centrally organize, coordinate, or initiate all contract updates, developers using this should be aware +that any updates executed using this design should ensure that their contract dependencies are either core contracts or +are entirely owned. + +### Considered Alternatives + +#### Atomized Contract Updates + +One considered alternative was to encapsulate contract updates on a single contract basis instead of bundling staged +deployments inside of `Updater`. These atomized contract updates could then be delegated to `Delegatee` and the +deployment ordered offchain based on the resolved dependency graph. This sorted update ordering could then be passed to +the `Delegatee` in batches, ensuring that all contracts are updated according to their dependencies. + +While this sounds much neater and is in essence the approach taken by centralized, top-down architectures, with more +investigation, this approach was revealed to be fragile. The thinking here is that since delegating updates cannot be +compulsory, the `Delegatee` will inevitably lack full global context and control over contract updates. We cannot take a +top-down approach in a system that is fundamentally bottom-up. + +The `Delegatee` will inevitably lack the ability to update some members of the full dependency graph. And if some of the +contracts that we are tasked with updating depend on those we can't update, all the effort we invest into neatly +resolving updates will ultimately fail. The lift for this approach is significantly higher than the proposed +self-defined crowdsourcing design for essentially similar strength guarantees. + +#### Hotswap Contracts + +Another alternative is to temporarily allow contract updates to take place intra-transaction by hotswapping contract +code. Hotswapping here means updating the execution state of the updated contract within the updating transaction +instead of at the end. This would allow for a bundle of contract updates to occur in a single transaction without the +need to batch updates into stages based on depth in their dependency graph. + +Taking the example mentioned [previously](#context), instead of executing updates to contracts `A`, `B`, and `C` in +discrete transactions, we would execute all three updates in a single transaction so long as the updates are +appropriately ordered. + +However, hotswapping contracts comes with its own security concerns, and likely demands a sizeable implementation effort +with unknown consequences presenting an undue burden for what is potentially a single-use feature. + +### Performance Implications + +As mentioned above, we'll want to examine performance benchmarks with a focus on `Updater` construction - more generally +saving update deployments onchain - and `Delegatee` update execution - more generally executing a large number of +contract updates in a single transaction. + +### Best Practices + +As mentioned above, [supporting resources](#supporting-resources), examples, and use guides should follow if this FLIP +is adopted. + +One thing to note though is that this design is not likely to cover use cases where the dependencies are not either core +contracts or fully owned by the developer since updates are not atomized and ordered at the level of `Delegatee`. + +### Examples + +For a working example, see [this repo and README walkthrough](https://github.com/sisyphusSmiling/contract-updater) +demonstrating the end-to-end setup and update execution flow. + +Note that the linked prototype utilizes the currently available `update__experimental()` API, but emulates the proposed +design in the context of persistent update mechanisms via happy path (i.e. successful contract updates). + +### Compatibility + +This design is fully compatible with existing and planned featuresets. The only dependency here is the addition of [the +aforementioned](#note-on-update-api) `Contracts.tryUpdate(): DeploymentResult` (issue found +[here](https://github.com/onflow/cadence/issues/2700)) which would enable batched updates without interruption. + +## Related Issues + +- [Issue: Staged Contract Update](https://github.com/onflow/cadence/issues/2019) +- [Tracker: Cadence 1.0](https://github.com/onflow/cadence/issues/2642) +- [Issue: Add `tryUpdate()` method](https://github.com/onflow/cadence/issues/2700) +- [Issue: Flow CLI Contract to hex util](https://github.com/onflow/flow-cli/issues/1157) + +## Prior Art + +- [Proof of concept implementation](https://github.com/sisyphusSmiling/contract-updater) + +## Questions & Discussion Topics + +- What are the transaction limits - how do they affect `Updater` setup and `Delegatee` update execution for large + numbers of accounts? +- What other persistent use cases would benefit from this mechanism? +- How many developers would delegate their contract updates if offered the service? \ No newline at end of file