From fc5a63b1e93d97d82e2a1885940279a99426ccff Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:46:28 -0600 Subject: [PATCH 01/40] update blockUpdateBoundary logic for unknown boundary contract init --- contracts/StagedContractUpdates.cdc | 85 ++++++++++++++----- ...ntract_delegatee_block_update_boundary.cdc | 5 ++ tests/staged_contract_updater_tests.cdc | 66 ++++++++------ .../coordinator/set_block_update_boundary.cdc | 17 +++- .../delegatee/set_block_update_boundary.cdc | 19 +++++ .../updater/setup_updater_multi_account.cdc | 6 +- ...up_updater_single_account_and_contract.cdc | 7 +- 7 files changed, 157 insertions(+), 48 deletions(-) create mode 100644 scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc create mode 100644 transactions/delegatee/set_block_update_boundary.cdc diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 1e69c2c..d557776 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -18,7 +18,7 @@ access(all) contract StagedContractUpdates { /// Common update boundary for those coordinating with contract account-managed Delegatee, enabling opt-in /// Flow coordinated contract updates - access(all) var blockUpdateBoundary: UInt64 + access(all) var blockUpdateBoundary: UInt64? /* --- Canonical Paths --- */ // @@ -47,7 +47,10 @@ access(all) contract StagedContractUpdates { failedContracts: [String], updateComplete: Bool ) + /// Represents a change in delegation status for an Updater - delegated: added to Delegatee, !delegated: removed access(all) event UpdaterDelegationChanged(updaterUUID: UInt64, updaterAddress: Address?, delegated: Bool) + /// Event emitted when a Delegatee's block update boundary is set, signifying the Delegatee is ready for delegation + access(all) event DelegateeBlockUpdateBoundarySet(delegateeUUID: UInt64, address: Address?, blockHeight: UInt64) /// Represents contract and its corresponding code /// @@ -160,7 +163,7 @@ access(all) contract StagedContractUpdates { /// access(all) resource Updater : UpdaterPublic, MetadataViews.Resolver { /// Update to occur at or beyond this block height - access(self) let blockUpdateBoundary: UInt64 + access(self) var blockUpdateBoundary: UInt64 /// Update status defining whether all update stages have been *attempted* /// NOTE: `true` does not necessarily mean all updates were successful access(self) var updateComplete: Bool @@ -324,6 +327,12 @@ access(all) contract StagedContractUpdates { } return nil } + + /* --- Delegatee access override --- */ + + access(contract) fun delegateeBoundaryOverride(_ newBoundary: UInt64) { + self.blockUpdateBoundary = newBoundary + } } /* --- Delegatee --- */ @@ -332,6 +341,7 @@ access(all) contract StagedContractUpdates { /// access(all) resource interface DelegateePublic { access(all) fun check(id: UInt64): Bool? + access(all) fun getBlockUpdateBoundary(): UInt64? access(all) fun getUpdaterIDs(): [UInt64] access(all) fun delegate(updaterCap: Capability<&Updater>) access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) @@ -343,13 +353,25 @@ access(all) contract StagedContractUpdates { /// Block height at which delegated updates will be performed by this Delegatee /// NOTE: This may differ from the contract's blockUpdateBoundary, enabling flexibility but any Updaters not /// ready when updates are performed will be revoked from the Delegatee - access(self) let blockUpdateBoundary: UInt64 + access(self) var blockUpdateBoundary: UInt64? /// Mapping of all delegated Updater Capabilities by their UUID access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>} - init(blockUpdateBoundary: UInt64) { + init(blockUpdateBoundary: UInt64?) { + pre { + blockUpdateBoundary == nil || blockUpdateBoundary! > getCurrentBlock().height: + "Block update boundary must be in the future!" + } self.blockUpdateBoundary = blockUpdateBoundary self.delegatedUpdaters = {} + + if blockUpdateBoundary != nil { + emit DelegateeBlockUpdateBoundarySet( + delegateeUUID: self.uuid, + address: nil, + blockHeight: blockUpdateBoundary! + ) + } } /// Checks if the specified DelegatedUpdater Capability is contained and valid @@ -358,6 +380,12 @@ access(all) contract StagedContractUpdates { return self.delegatedUpdaters[id]?.check() ?? nil } + /// Getter for block update boundary + /// + access(all) fun getBlockUpdateBoundary(): UInt64? { + return self.blockUpdateBoundary + } + /// Returns the IDs of delegated Updaters /// access(all) fun getUpdaterIDs(): [UInt64] { @@ -368,15 +396,19 @@ access(all) contract StagedContractUpdates { /// access(all) fun delegate(updaterCap: Capability<&Updater>) { pre { - getCurrentBlock().height < self.blockUpdateBoundary: - "Delegation must occur before Delegatee boundary of ".concat(self.blockUpdateBoundary.toString()) + self.blockUpdateBoundary != nil: + "Delegation is not yet enabled, wait for Delegatee to set block update boundary" + getCurrentBlock().height < self.blockUpdateBoundary!: + "Delegation must occur before Delegatee boundary of ".concat(self.blockUpdateBoundary!.toString()) updaterCap.check(): "Invalid DelegatedUpdater Capability!" updaterCap.borrow()!.hasBeenUpdated() == false: "Updater has already been updated!" - updaterCap.borrow()!.getBlockUpdateBoundary() <= self.blockUpdateBoundary: - "Updater will not be ready for updates at Delegatee boundary of ".concat(self.blockUpdateBoundary.toString()) + updaterCap.borrow()!.getBlockUpdateBoundary() <= self.blockUpdateBoundary!: + "Updater will not be ready for updates at Delegatee boundary of ".concat(self.blockUpdateBoundary!.toString()) } - let updater: &StagedContractUpdates.Updater = updaterCap.borrow()! + + updater.delegateeBoundaryOverride(self.blockUpdateBoundary!) + if self.delegatedUpdaters.containsKey(updater.getID()) { // Upsert if updater Capability already contained self.delegatedUpdaters[updater.getID()] = updaterCap @@ -399,6 +431,21 @@ access(all) contract StagedContractUpdates { self.removeDelegatedUpdater(id: updater.getID()) } + /// Enables the Delegatee to set the block update boundary if it wasn't set on init, enabling delegation + /// + access(all) fun setBlockUpdateBoundary(blockHeight: UInt64) { + pre { + self.blockUpdateBoundary == nil: "Update boundary has already been set" + blockHeight > getCurrentBlock().height: "Block update boundary must be in the future!" + } + self.blockUpdateBoundary = blockHeight + emit DelegateeBlockUpdateBoundarySet( + delegateeUUID: self.uuid, + address: self.owner!.address, + blockHeight: blockHeight + ) + } + /// Executes update on the specified Updaters. All updates are attempted, and if the Updater is not yet ready /// to be updated (updater.update() returns nil) or the attempted update is the final staged (updater.update() /// returns true), the corresponding Updater Capability is removed. @@ -420,6 +467,7 @@ access(all) contract StagedContractUpdates { // Execute currently staged update let success: Bool? = updaterCap.borrow()!.update() // If update is not ready or complete, remove Capability and continue + // NOTE: Checked to prevent revert in case the Updater resource our Capability targets was swapped if success == nil || success! == true { self.delegatedUpdaters.remove(key: id) continue @@ -452,9 +500,10 @@ access(all) contract StagedContractUpdates { access(all) fun setBlockUpdateBoundary(new: UInt64) { pre { new > getCurrentBlock().height: "New boundary must be in the future!" - new > StagedContractUpdates.blockUpdateBoundary: "New block update boundary must be greater than current boundary!" + StagedContractUpdates.blockUpdateBoundary == nil || new > StagedContractUpdates.blockUpdateBoundary!: + "New block update boundary must be greater than current boundary!" } - let old = StagedContractUpdates.blockUpdateBoundary + let old: UInt64? = StagedContractUpdates.blockUpdateBoundary StagedContractUpdates.blockUpdateBoundary = new emit ContractBlockUpdateBoundaryUpdated(old: old, new: new) } @@ -466,7 +515,7 @@ access(all) contract StagedContractUpdates { /// access(all) fun getContractDelegateeCapability(): Capability<&{DelegateePublic}> { let delegateeCap = self.account.getCapability<&{DelegateePublic}>(self.DelegateePublicPath) - assert(delegateeCap.check(), message: "Invalid Delegatee Capability retrieved") + assert(delegateeCap.check(), message: "Invalid Delegatee Capability retrieved from contract account") return delegateeCap } @@ -525,14 +574,14 @@ access(all) contract StagedContractUpdates { /// Creates a new Delegatee resource enabling caller to self-host their Delegatee to be executed at or beyond /// given block update boundary /// - access(all) fun createNewDelegatee(blockUpdateBoundary: UInt64): @Delegatee { + access(all) fun createNewDelegatee(blockUpdateBoundary: UInt64?): @Delegatee { return <- create Delegatee(blockUpdateBoundary: blockUpdateBoundary) } - init(blockUpdateBoundary: UInt64) { + init() { let contractAddress = self.account.address.toString() - self.blockUpdateBoundary = blockUpdateBoundary + self.blockUpdateBoundary = nil self.inboxHostCapabilityNamePrefix = "StagedContractUpdatesHostCapability_" self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! @@ -542,11 +591,9 @@ access(all) contract StagedContractUpdates { self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(contractAddress))! self.CoordinatorStoragePath = StoragePath(identifier: "StagedContractUpdatesCoordinator_".concat(contractAddress))! - self.account.save(<-create Delegatee(blockUpdateBoundary: blockUpdateBoundary), to: self.DelegateeStoragePath) + self.account.save(<-create Delegatee(blockUpdateBoundary: nil), to: self.DelegateeStoragePath) self.account.link<&{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) self.account.save(<-create Coordinator(), to: self.CoordinatorStoragePath) - - emit ContractBlockUpdateBoundaryUpdated(old: nil, new: blockUpdateBoundary) } -} +} diff --git a/scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc b/scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc new file mode 100644 index 0000000..5deb38c --- /dev/null +++ b/scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc @@ -0,0 +1,5 @@ +import "StagedContractUpdates" + +access(all) fun main(): UInt64? { + return StagedContractUpdates.getContractDelegateeCapability().borrow()?.getBlockUpdateBoundary() ?? nil +} diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 3e68b99..c4ed5a0 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -23,7 +23,7 @@ access(all) fun setup() { var err = Test.deployContract( name: "StagedContractUpdates", path: "../contracts/StagedContractUpdates.cdc", - arguments: [getCurrentBlockHeight() + blockHeightBoundaryDelay] + arguments: [] ) Test.expect(err, Test.beNil()) @@ -56,6 +56,45 @@ access(all) fun setup() { Test.expect(err, Test.beNil()) } +access(all) fun testCoordinatorSetBlockUpdateBoundaryFails() { + let txResult = executeTransaction( + "../transactions/coordinator/set_block_update_boundary.cdc", + [1], + admin + ) + Test.expect(txResult, Test.beFailed()) +} + +access(all) fun testCoordinatorSetBlockUpdateBoundarySucceeds() { + let expectedBoundary = getCurrentBlockHeight() + blockHeightBoundaryDelay + let txResult = executeTransaction( + "../transactions/coordinator/set_block_update_boundary.cdc", + [expectedBoundary], + admin + ) + Test.expect(txResult, Test.beSucceeded()) + + // TODO: Uncomment once bug is fixed allowing contract import + // let actualBoundary = StagedContractUpdates.blockUpdateBoundary + // Test.assertEqual(expectedBoundary, actualBoundary) + // events = Test.eventsOfType(Type()) + // Test.assertEqual(1, events.length) +} + +access(all) fun testDelegateeSetBlockUpdateBoundarySucceeds() { + let expectedBoundary = getCurrentBlockHeight() + blockHeightBoundaryDelay + let txResult = executeTransaction( + "../transactions/delegatee/set_block_update_boundary.cdc", + [expectedBoundary], + admin + ) + Test.expect(txResult, Test.beSucceeded()) + + let actualBoundary = executeScript("../scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc", []).returnValue as! UInt64? + ?? panic("Problem retrieving block update boundary") + Test.assertEqual(expectedBoundary, actualBoundary) +} + access(all) fun testEmptyDeploymentUpdaterInitFails() { let alice = Test.createAccount() let txResult = executeTransaction( @@ -66,7 +105,7 @@ access(all) fun testEmptyDeploymentUpdaterInitFails() { Test.expect(txResult, Test.beFailed()) } -access(all) fun testSetupMultiContractMultiAccountUpdater() { +access(all) fun testSetupMultiContractMultiAccountUpdaterSucceeds() { let contractAddresses: [Address] = [aAccount.address, bcAccount.address] let stage0: [{Address: {String: String}}] = [ { @@ -111,6 +150,7 @@ access(all) fun testSetupMultiContractMultiAccountUpdater() { [nil, contractAddresses, deploymentConfig], abcUpdater ) + Test.expect(setupUpdaterTxResult, Test.beSucceeded()) // Confirm UpdaterCreated event was properly emitted // TODO: Uncomment once bug is fixed allowing contract import @@ -322,28 +362,6 @@ access(all) fun testDelegationOfCompletedUpdaterFails() { Test.expect(txResult, Test.beFailed()) } -access(all) fun testCoordinatorSetBlockUpdateBoundaryFails() { - let txResult = executeTransaction( - "../transactions/coordinator/set_block_update_boundary.cdc", - [1], - admin - ) - Test.expect(txResult, Test.beFailed()) -} - -access(all) fun testCoordinatorSetBlockUpdateBoundarySucceeds() { - let txResult = executeTransaction( - "../transactions/coordinator/set_block_update_boundary.cdc", - [getCurrentBlockHeight() + blockHeightBoundaryDelay], - admin - ) - Test.expect(txResult, Test.beSucceeded()) - - // TODO: Uncomment once bug is fixed allowing contract import - // events = Test.eventsOfType(Type()) - // Test.assertEqual(1, events.length) -} - /* --- TEST HELPERS --- */ access(all) fun jumpToUpdateBoundary(forUpdater: Address) { diff --git a/transactions/coordinator/set_block_update_boundary.cdc b/transactions/coordinator/set_block_update_boundary.cdc index a16d653..5c16b0f 100644 --- a/transactions/coordinator/set_block_update_boundary.cdc +++ b/transactions/coordinator/set_block_update_boundary.cdc @@ -3,9 +3,20 @@ import "StagedContractUpdates" /// Allows the contract Coordinator to set a new blockUpdateBoundary /// transaction(newBoundary: UInt64) { + + let coordinator: &StagedContractUpdates.Coordinator + prepare(signer: AuthAccount) { - signer.borrow<&StagedContractUpdates.Coordinator>(from: StagedContractUpdates.CoordinatorStoragePath) - ?.setBlockUpdateBoundary(new: newBoundary) - ?? panic("Could not borrow reference to Coordinator!") + self.coordinator = signer.borrow<&StagedContractUpdates.Coordinator>( + from: StagedContractUpdates.CoordinatorStoragePath + ) ?? panic("Could not borrow reference to Coordinator!") + } + + execute { + self.coordinator.setBlockUpdateBoundary(new: newBoundary) + } + + post { + StagedContractUpdates.blockUpdateBoundary == newBoundary: "Problem setting block update boundary" } } diff --git a/transactions/delegatee/set_block_update_boundary.cdc b/transactions/delegatee/set_block_update_boundary.cdc new file mode 100644 index 0000000..e01d68a --- /dev/null +++ b/transactions/delegatee/set_block_update_boundary.cdc @@ -0,0 +1,19 @@ +import "StagedContractUpdates" + +transaction(blockUpdateBoundary: UInt64) { + + let delegatee: &StagedContractUpdates.Delegatee + + prepare(signer: AuthAccount) { + self.delegatee = signer.borrow<&StagedContractUpdates.Delegatee>(from: StagedContractUpdates.DelegateeStoragePath) + ?? panic("Could not borrow a reference to the signer's Delegatee") + } + + execute { + self.delegatee.setBlockUpdateBoundary(blockHeight: blockUpdateBoundary) + } + + post { + self.delegatee.getBlockUpdateBoundary() == blockUpdateBoundary: "Problem setting block update boundary" + } +} diff --git a/transactions/updater/setup_updater_multi_account.cdc b/transactions/updater/setup_updater_multi_account.cdc index 2f6ed62..b68c0bc 100644 --- a/transactions/updater/setup_updater_multi_account.cdc +++ b/transactions/updater/setup_updater_multi_account.cdc @@ -37,9 +37,13 @@ transaction(blockHeightBoundary: UInt64?, contractAddresses: [Address], deployme // Construct deployment from config let deployments: [[StagedContractUpdates.ContractUpdate]] = StagedContractUpdates.getDeploymentFromConfig(deploymentConfig) + if blockHeightBoundary == nil && StagedContractUpdates.blockUpdateBoundary == nil { + // TODO: THIS IS A PROBLEM - Can't setup Updater without a contract blockHeightBoundary + panic("Contract update boundary is not yet set, must specify blockHeightBoundary if not coordinating") + } // Construct the updater, save and link public Capability let contractUpdater: @StagedContractUpdates.Updater <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary, + blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary!, hosts: hostCaps, deployments: deployments ) diff --git a/transactions/updater/setup_updater_single_account_and_contract.cdc b/transactions/updater/setup_updater_single_account_and_contract.cdc index eda6d07..86ef685 100644 --- a/transactions/updater/setup_updater_single_account_and_contract.cdc +++ b/transactions/updater/setup_updater_single_account_and_contract.cdc @@ -41,10 +41,15 @@ transaction(blockHeightBoundary: UInt64?, contractName: String, code: String) { } let hostCap = signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath) + if blockHeightBoundary == nil && StagedContractUpdates.blockUpdateBoundary == nil { + // TODO: THIS IS A PROBLEM - Can't setup Updater without a contract blockHeightBoundary + // Maybe alt is to set to now + 1? + panic("Contract update boundary is not yet set, must specify blockHeightBoundary if not coordinating") + } // Create Updater resource, assigning the contract .blockUpdateBoundary to the new Updater signer.save( <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary, + blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary!, hosts: [hostCap], deployments: [[ StagedContractUpdates.ContractUpdate( From 12c2eb92ea7a4a2053c69158bacc11949da93bfd Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:09:23 -0600 Subject: [PATCH 02/40] add MigrationContractStaging contract supporting 1.0 migration --- contracts/MigrationContractStaging.cdc | 259 +++++++++++++++++++++++++ flow.json | 16 +- 2 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 contracts/MigrationContractStaging.cdc diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc new file mode 100644 index 0000000..6e2b498 --- /dev/null +++ b/contracts/MigrationContractStaging.cdc @@ -0,0 +1,259 @@ +/// This contract is intended for use for the Cadence 1.0 contract migration across the Flow network. +/// +/// To stage your contract for automated updates in preparation for Cadence 1.0, simply configure an Updater resource +/// in the contract account along with an AuthAccount Capability, the contract address, name and hex-encoded Cadence. +/// +/// NOTE: Do not move the Updater resource from this account or unlink the Account Capability in your Host or you risk +/// breaking the automated update process for your contract. +/// +access(all) contract MigrationContractStaging { + + // Path derivation constants + // + access(all) let delimiter: String + access(all) let updaterPathPrefix: String + access(all) let accountCapabilityPathPrefix: String + + /// Event emitted when an Updater is created + /// NOTE: Does not guarantee that the Updater is properly configured or even exists beyond event emission + access(all) event AccountContractStaged( + updaterUUID: UInt64, + address: Address, + codeHash: [UInt8], + contract: String + ) + + /* --- ContractUpdate --- */ + // + /// Represents contract and its corresponding code + /// + access(all) struct ContractUpdate { + access(all) let address: Address + access(all) let name: String + access(all) let code: String + + init(address: Address, name: String, code: String) { + self.address = address + self.name = name + self.code = code + } + + access(all) view fun verify(): Bool { + return getAccount(self.address).contracts.borrow<&AnyStruct>(name: self.name) != nil + } + + /// Serializes the address and name into a string + access(all) view fun toString(): String { + return self.address.toString().concat(".").concat(self.name) + } + + /// Returns code as a String + access(all) view fun codeAsCadence(): String { + return String.fromUTF8(self.code.decodeHex()) ?? panic("Problem stringifying code!") + } + } + + /* --- Host --- */ + // + /// Encapsulates an AuthAccount, exposing only the ability to update contracts on the underlying account + /// + access(all) resource Host { + /// Capability on the underlying account, possession of which serves as proof of access on the account + access(self) let accountCapability: Capability<&AuthAccount> + + init(accountCapability: Capability<&AuthAccount>) { + self.accountCapability = accountCapability + } + + /// Verifies that the encapsulated Account is the owner of this Host + /// + access(all) view fun verify(): Bool { + return self.getHostAddress() != nil && self.getHostAddress() == self.owner?.address + } + + /// Checks the wrapped AuthAccount Capability + /// + access(all) view fun checkAccountCapability(): Bool { + return self.accountCapability.check() + } + + /// Returns the Address of the underlying account + /// + access(all) view fun getHostAddress(): Address? { + return self.accountCapability.borrow()?.address + } + } + + /* --- Updater --- */ + // + /// Resource that enables staged contract updates to the Host account. In the context of the Cadence 1.0 migration, + /// this Updater should be stored in the account which is to be updated. + /// + access(all) resource Updater { + access(self) let host: @Host + access(self) let stagedUpdate: ContractUpdate + + init(host: @Host, stagedUpdate: ContractUpdate) { + pre { + host.getHostAddress() == stagedUpdate.address: "Host and update address must match" + stagedUpdate.codeAsCadence() != nil: "Staged update code must be valid Cadence" + stagedUpdate.verify(): "Target contract does not exist" + } + self.host <- host + self.stagedUpdate = stagedUpdate + } + + /// Returns whether the Updater is properly configured + /// NOTE: Does NOT check that the staged contract code is valid! + /// + access(all) view fun verify(): Bool { + let checks: {String: String} = self.statusCheck() + for status in checks.values { + if status != "PASSING" { + return false + } + } + return true + } + + /// Enables borrowing of a Host reference. Since all Host methods are view, this is safe to do. + /// + access(all) view fun borrowHost(): &Host { + return &self.host as &Host + } + + /// Returns the staged contract update in the form of a ContractUpdate struct + /// + access(all) view fun getContractUpdate(): ContractUpdate { + return self.stagedUpdate + } + + /// Checks all conditions **EXCEPT CODE VALIDITY** and reports back with a map of statuses for each check + /// Platforms may utilize this method to display whether the Updater is properly configured in human-readable + /// language + /// + access(all) view fun statusCheck(): {String: String} { + return { + "Updater": self.statusCheckUpdater(), + "Host": self.statusCheckHost(), + "Staged Update": self.statusCheckStagedUpdate(), + "Contract Existence": self.statusCheckTargetContractExists() + } + } + + /// Returns the status of the Updater based on conditions relevant to the Cadence 1.0 contract migration + /// + access(all) view fun statusCheckUpdater(): String { + if self.owner == nil { + return "FAILING: Unowned Updater" + } else if self.owner!.address == self.host.getHostAddress() && + self.owner!.address == self.stagedUpdate.address { + return "PASSING" + } else if self.owner!.address != self.stagedUpdate.address { + return "FAILING: Owner address does not match staged Update target address" + } else if self.owner!.address != self.host.getHostAddress() { + return "FAILING: Owner address does not match Host address" + } else { + return "FAILING: Unknown error" + } + } + + /// Returns the status of the Host based on conditions relevant to the Cadence 1.0 contract migration + /// + access(all) view fun statusCheckHost(): String { + if self.host.verify() { + return "PASSING" + } else if self.host.checkAccountCapability() == false { + return "FAILING: Account Capability check failed" + } else if self.owner == nil { + return "FAILING: Unowned Updater" + } else if self.host.getHostAddress()! != self.owner!.address { + return "FAILING: Host address does not match owner address" + } else { + return "FAILING: Unknown error" + } + } + + /// Returns the status of the staged update based on conditions relevant to the Cadence 1.0 contract migration + /// + access(all) view fun statusCheckStagedUpdate(): String { + if self.stagedUpdate.address != self.host.getHostAddress() { + return "FAILING: Staged Update address does not match Host address" + } else if self.owner == nil { + return "FAILING: Unowned Updater" + } else if self.stagedUpdate.address == self.host.getHostAddress() { + return "PASSING" + } else { + return "FAILING: Unknown error" + } + } + + /// Returns whether the staged contract exists on the target account. This is important as the contract + /// migration only affects **existing** contracts + /// + access(all) view fun statusCheckTargetContractExists(): String { + if self.stagedUpdate.verify() { + return "PASSING" + } else { + return "FAILING: Target contract with name " + .concat(self.stagedUpdate.name) + .concat(" does not exist at address ") + .concat(self.stagedUpdate.address.toString()) + } + } + + destroy() { + destroy self.host + } + } + + /// Returns a StoragePath to store the Updater of the form: + /// /storage/self.updaterPathPrefix_ADDRESS_NAME + access(all) fun deriveUpdaterStoragePath(contractAddress: Address, contractName: String): StoragePath { + return StoragePath( + identifier: self.updaterPathPrefix + .concat(self.delimiter) + .concat(contractAddress.toString()) + .concat(self.delimiter) + .concat(contractName) + ) ?? panic("Could not derive Updater StoragePath for given address") + } + + /// Returns a PrivatePath to store the Account Capability of the form: + /// /storage/self.accountCapabilityPathPrefix_ADDRESS + access(all) fun deriveAccountCapabilityPath(forAddress: Address): PrivatePath { + return PrivatePath( + identifier: self.accountCapabilityPathPrefix.concat(self.delimiter).concat(forAddress.toString()) + ) ?? panic("Could not derive Account Capability path for given address") + } + + /// Creates a Host resource wrapping the given account capability + /// + access(all) fun createHost(accountCapability: Capability<&AuthAccount>): @Host { + return <- create Host(accountCapability: accountCapability) + } + + /// Creates an Updater resource with the given Host and ContractUpdate. Should be stored at the derived path in the + /// target address - the same account the Host maintains an Account Capability for. + /// + access(all) fun createUpdater(host: @Host, stagedUpdate: ContractUpdate): @Updater { + let updater: @MigrationContractStaging.Updater <- create Updater(host: <-host, stagedUpdate: stagedUpdate) + emit AccountContractStaged( + updaterUUID: updater.uuid, + address: stagedUpdate.address, + codeHash: stagedUpdate.code.decodeHex(), + contract: stagedUpdate.name + ) + return <- updater + } + + init() { + self.delimiter = "_" + self.accountCapabilityPathPrefix = "MigrationContractStagingHostAccountCapability" + .concat(self.delimiter) + .concat(self.account.address.toString()) + self.updaterPathPrefix = "MigrationContractStagingUpdater" + .concat(self.delimiter) + .concat(self.account.address.toString()) + } +} diff --git a/flow.json b/flow.json index d7bb2ad..c7106fa 100644 --- a/flow.json +++ b/flow.json @@ -44,6 +44,12 @@ "testnet": "631e88ae7f1d7c20" } }, + "MigrationContractStaging": { + "source": "./contracts/MigrationContractStaging.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7" + } + }, "NonFungibleToken": { "source": "./contracts/standards/NonFungibleToken.cdc", "aliases": { @@ -103,15 +109,7 @@ "C" ], "emulator-account": [ - { - "name": "StagedContractUpdates", - "args": [ - { - "type": "UInt64", - "value": "10" - } - ] - } + "MigrationContractStaging" ], "emulator-ft": [ "FungibleToken" From 6b1fb6826c8140dda0c6264db319f56f27676e7b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:54:44 -0600 Subject: [PATCH 03/40] Expand on UpdaterCreated event data --- contracts/StagedContractUpdates.cdc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index d557776..174d958 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -1,5 +1,9 @@ import "MetadataViews" +/******************************************************************************************************************* + NOTICE: THIS CONTRACT IS NOT INTENDED FOR CADENCE 1.0 STAGING - SEE `MigrationContractStaging` FOR 1.0 STAGING + *******************************************************************************************************************/ + /// This contract defines resources which enable storage of contract code for the purposes of updating at or beyond /// some blockheight boundary either by the containing resource's owner or by some delegated party. /// @@ -34,7 +38,7 @@ access(all) contract StagedContractUpdates { /// Event emitted when the contract block update boundary is updated access(all) event ContractBlockUpdateBoundaryUpdated(old: UInt64?, new: UInt64) /// Event emitted when an Updater is created - access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) + access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64, stagedContracts: [String]) /// Event emitted when an Updater is updated access(all) event UpdaterUpdated( updaterUUID: UInt64, @@ -567,7 +571,13 @@ access(all) contract StagedContractUpdates { deployments: [[ContractUpdate]] ): @Updater { let updater <- create Updater(blockUpdateBoundary: blockUpdateBoundary, hosts: hosts, deployments: deployments) - emit UpdaterCreated(updaterUUID: updater.uuid, blockUpdateBoundary: blockUpdateBoundary) + let contracts: [String] = [] + for deployment in deployments { + for update in deployment { + contracts.append(update.toString()) + } + } + emit UpdaterCreated(updaterUUID: updater.uuid, blockUpdateBoundary: blockUpdateBoundary, stagedContracts: contracts) return <- updater } From e73e2449d3705b4fe45320fa405a9b6b55ae6dd5 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:55:22 -0600 Subject: [PATCH 04/40] add deriveUpdaterPublicPath method --- contracts/MigrationContractStaging.cdc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 6e2b498..3ec8163 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -219,6 +219,18 @@ access(all) contract MigrationContractStaging { ) ?? panic("Could not derive Updater StoragePath for given address") } + /// Returns a PublicPath to store the Updater of the form: + /// /storage/self.updaterPathPrefix_ADDRESS_NAME + access(all) fun deriveUpdaterPublicPath(contractAddress: Address, contractName: String): PublicPath { + return PublicPath( + identifier: self.updaterPathPrefix + .concat(self.delimiter) + .concat(contractAddress.toString()) + .concat(self.delimiter) + .concat(contractName) + ) ?? panic("Could not derive Updater PublicPath for given address") + } + /// Returns a PrivatePath to store the Account Capability of the form: /// /storage/self.accountCapabilityPathPrefix_ADDRESS access(all) fun deriveAccountCapabilityPath(forAddress: Address): PrivatePath { From d246278080d78d8ff9d340960d24a0331c611c5c Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:56:21 -0600 Subject: [PATCH 05/40] add stage & unstage contract transactions --- .../stage_contract.cdc | 59 +++++++++++++++++++ .../unstage_contract.cdc | 22 +++++++ 2 files changed, 81 insertions(+) create mode 100644 transactions/migration-contract-staging/stage_contract.cdc create mode 100644 transactions/migration-contract-staging/unstage_contract.cdc diff --git a/transactions/migration-contract-staging/stage_contract.cdc b/transactions/migration-contract-staging/stage_contract.cdc new file mode 100644 index 0000000..363faad --- /dev/null +++ b/transactions/migration-contract-staging/stage_contract.cdc @@ -0,0 +1,59 @@ +#allowAccountLinking + +import "MigrationContractStaging" + +/// This transaction is used to stage a contract update for Cadence 1.0 contract migrations. +/// Ensure that this transaction is signed by the account that owns the contract to be updated and that the contract +/// has already been deployed to the signing account. +/// +/// For more context, see the repo - https://github.com/onflow/contract-updater +/// +/// @param contractName: The name of the contract to be updated with the given code +/// @param contractCode: The updated contract code as a hex-encoded String +/// +transaction(contractName: String, contractCode: String) { + let accountCapability: Capability<&AuthAccount> + + prepare(signer: AuthAccount) { + // Retrieve an AuthAccount Capability to the signer's account + let accountCapabilityPath: PrivatePath = MigrationContractStaging.deriveAccountCapabilityPath( + forAddress: signer.address + ) + if signer.getCapability<&AuthAccount>(accountCapabilityPath).borrow() == nil { + self.accountCapability = signer.linkAccount(accountCapabilityPath) ?? panic("Problem linking account") + } else { + self.accountCapability = signer.getCapability<&AuthAccount>(accountCapabilityPath) + } + // Create a Host resource, wrapping the retrieved Account Capability + let host: @MigrationContractStaging.Host <- MigrationContractStaging.createHost( + accountCapability: self.accountCapability + ) + + // Create an Updater resource, staging the update + let updaterStoragePath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( + contractAddress: signer.address, + contractName: contractName + ) + // Ensure that an Updater resource doesn't already exist for this contract. If so, revert. Signer should + // inspect the existing Updater and destroy if needed before re-attempting this transaction + assert( + signer.borrow<&MigrationContractStaging.Updater>(from: updaterStoragePath) == nil, + message: "Updater already exists" + ) + signer.save( + <-MigrationContractStaging.createUpdater( + host: <-host, + stagedUpdate: MigrationContractStaging.ContractUpdate( + address: signer.address, + name: contractName, + code: contractCode + ) + ), to: updaterStoragePath + ) + let updaterPublicPath: PublicPath = MigrationContractStaging.deriveUpdaterPublicPath( + contractAddress: signer.address, + contractName: contractName + ) + signer.link<&MigrationContractStaging.Updater>(updaterPublicPath, target: updaterStoragePath) + } +} diff --git a/transactions/migration-contract-staging/unstage_contract.cdc b/transactions/migration-contract-staging/unstage_contract.cdc new file mode 100644 index 0000000..0b9ea44 --- /dev/null +++ b/transactions/migration-contract-staging/unstage_contract.cdc @@ -0,0 +1,22 @@ +import "MigrationContractStaging" + +/// Loads and destroys the existing updater for the given contract name in the signer's account if exists. This means +/// the contract will no longer be staged for migrated updates. +/// +/// For more context, see the repo - https://github.com/onflow/contract-updater +/// +transaction(contractName: String) { + + prepare(signer: AuthAccount) { + let updaterStoragePath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( + contractAddress: signer.address, + contractName: contractName + ) + let updaterPublicPath: PublicPath = MigrationContractStaging.deriveUpdaterPublicPath( + contractAddress: signer.address, + contractName: contractName + ) + signer.unlink(updaterPublicPath) + destroy signer.load<@MigrationContractStaging.Updater>(from: updaterStoragePath) + } +} From 17a987dc7af87770b325c03b61048ebb01c77351 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:16:28 -0600 Subject: [PATCH 06/40] add migration supporting scripts --- .../get_staged_contract_code.cdc | 12 ++++++++++++ .../status_check.cdc | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 scripts/migration-contract-staging/get_staged_contract_code.cdc create mode 100644 scripts/migration-contract-staging/status_check.cdc diff --git a/scripts/migration-contract-staging/get_staged_contract_code.cdc b/scripts/migration-contract-staging/get_staged_contract_code.cdc new file mode 100644 index 0000000..b9bd4bc --- /dev/null +++ b/scripts/migration-contract-staging/get_staged_contract_code.cdc @@ -0,0 +1,12 @@ +import "MigrationContractStaging" + +/// Returns the code as it is staged or nil if it not currently staged. +/// +access(all) fun main(contractAddress: Address, contractName: String): String? { + let updaterPath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( + contractAddress: contractAddress, contractName: contractName + ) + return getAuthAccount(contractAddress).borrow<&MigrationContractStaging.Updater>(from: updaterPath) + ?.getContractUpdate() + ?.codeAsCadence() +} diff --git a/scripts/migration-contract-staging/status_check.cdc b/scripts/migration-contract-staging/status_check.cdc new file mode 100644 index 0000000..fb70523 --- /dev/null +++ b/scripts/migration-contract-staging/status_check.cdc @@ -0,0 +1,18 @@ +import "MigrationContractStaging" + +/// Returns a mapping of status checks on various conditions affecting contract staging or nil if not staged. +/// A successful status check will look like: +/// { +/// "Updater": "PASSING", +/// "Host": "PASSING", +/// "Staged Update": "PASSING", +/// "Contract Existence": "PASSING" +/// } +/// If a status check fails, the value will be "FAILING:" followed by the reason for failure +/// +access(all) fun main(contractAddress: Address, contractName: String): {String: String}? { + let updaterPath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( + contractAddress: contractAddress, contractName: contractName + ) + return getAuthAccount(contractAddress).borrow<&MigrationContractStaging.Updater>(from: updaterPath)?.statusCheck() +} From 1a10aca5ab79b69f8c1cc249e1fad9d8b58e973e Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:16:48 -0600 Subject: [PATCH 07/40] fix Updater setup bug in MigrationContractStaging --- contracts/MigrationContractStaging.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 3ec8163..ce73c4a 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -39,7 +39,7 @@ access(all) contract MigrationContractStaging { } access(all) view fun verify(): Bool { - return getAccount(self.address).contracts.borrow<&AnyStruct>(name: self.name) != nil + return getAccount(self.address).contracts.names.contains(self.name) != nil } /// Serializes the address and name into a string From 3603d0fc4e672a15e225938947868e7a548228ee Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:29:55 -0600 Subject: [PATCH 08/40] update setup comments --- transactions/updater/setup_updater_multi_account.cdc | 2 +- .../updater/setup_updater_single_account_and_contract.cdc | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/transactions/updater/setup_updater_multi_account.cdc b/transactions/updater/setup_updater_multi_account.cdc index b68c0bc..0bbfb05 100644 --- a/transactions/updater/setup_updater_multi_account.cdc +++ b/transactions/updater/setup_updater_multi_account.cdc @@ -38,7 +38,7 @@ transaction(blockHeightBoundary: UInt64?, contractAddresses: [Address], deployme let deployments: [[StagedContractUpdates.ContractUpdate]] = StagedContractUpdates.getDeploymentFromConfig(deploymentConfig) if blockHeightBoundary == nil && StagedContractUpdates.blockUpdateBoundary == nil { - // TODO: THIS IS A PROBLEM - Can't setup Updater without a contract blockHeightBoundary + // TODO: Refactor contract for generalized cases as can't setup Updater without a contract blockHeightBoundary panic("Contract update boundary is not yet set, must specify blockHeightBoundary if not coordinating") } // Construct the updater, save and link public Capability diff --git a/transactions/updater/setup_updater_single_account_and_contract.cdc b/transactions/updater/setup_updater_single_account_and_contract.cdc index 86ef685..fb86e7e 100644 --- a/transactions/updater/setup_updater_single_account_and_contract.cdc +++ b/transactions/updater/setup_updater_single_account_and_contract.cdc @@ -42,8 +42,7 @@ transaction(blockHeightBoundary: UInt64?, contractName: String, code: String) { let hostCap = signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath) if blockHeightBoundary == nil && StagedContractUpdates.blockUpdateBoundary == nil { - // TODO: THIS IS A PROBLEM - Can't setup Updater without a contract blockHeightBoundary - // Maybe alt is to set to now + 1? + // TODO: Refactor contract for generalized cases as can't setup Updater without a contract blockHeightBoundary panic("Contract update boundary is not yet set, must specify blockHeightBoundary if not coordinating") } // Create Updater resource, assigning the contract .blockUpdateBoundary to the new Updater From 6e9f8d21b6170c429cd7471a9b2656d978980170 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:04:13 -0600 Subject: [PATCH 09/40] fix MigrationContractStaging.ContractUpdate.verify --- contracts/MigrationContractStaging.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index ce73c4a..1a9ed7d 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -39,7 +39,7 @@ access(all) contract MigrationContractStaging { } access(all) view fun verify(): Bool { - return getAccount(self.address).contracts.names.contains(self.name) != nil + return getAccount(self.address).contracts.names.contains(self.name) } /// Serializes the address and name into a string From dc2c67230d3a7b972b640739bcda7c2de28fa742 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:55:49 -0600 Subject: [PATCH 10/40] add contract comments + rename statusCheck to systems check --- contracts/MigrationContractStaging.cdc | 47 +++++++++++-------- .../{status_check.cdc => systems_check.cdc} | 2 +- 2 files changed, 28 insertions(+), 21 deletions(-) rename scripts/migration-contract-staging/{status_check.cdc => systems_check.cdc} (93%) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 1a9ed7d..8bef0c4 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -38,16 +38,20 @@ access(all) contract MigrationContractStaging { self.code = code } - access(all) view fun verify(): Bool { + /// Validates that the named contract exists at the target address + /// + access(all) view fun isValid(): Bool { return getAccount(self.address).contracts.names.contains(self.name) } - /// Serializes the address and name into a string + /// Serializes the address and name into a string of the form 0xADDRESS.NAME + /// access(all) view fun toString(): String { return self.address.toString().concat(".").concat(self.name) } - /// Returns code as a String + /// Returns human-readable string of the Cadence code + /// access(all) view fun codeAsCadence(): String { return String.fromUTF8(self.code.decodeHex()) ?? panic("Problem stringifying code!") } @@ -67,7 +71,7 @@ access(all) contract MigrationContractStaging { /// Verifies that the encapsulated Account is the owner of this Host /// - access(all) view fun verify(): Bool { + access(all) view fun isValid(): Bool { return self.getHostAddress() != nil && self.getHostAddress() == self.owner?.address } @@ -87,17 +91,20 @@ access(all) contract MigrationContractStaging { /* --- Updater --- */ // /// Resource that enables staged contract updates to the Host account. In the context of the Cadence 1.0 migration, - /// this Updater should be stored in the account which is to be updated. + /// this Updater should be stored with a ContractUpdate in the account which is to be updated. An Updater should be + /// configured for each contract in an account. /// access(all) resource Updater { + /// The Host resource encapsulating the Account Capability access(self) let host: @Host + /// The address, name and code of the contract that will be updated access(self) let stagedUpdate: ContractUpdate init(host: @Host, stagedUpdate: ContractUpdate) { pre { host.getHostAddress() == stagedUpdate.address: "Host and update address must match" stagedUpdate.codeAsCadence() != nil: "Staged update code must be valid Cadence" - stagedUpdate.verify(): "Target contract does not exist" + stagedUpdate.isValid(): "Target contract does not exist" } self.host <- host self.stagedUpdate = stagedUpdate @@ -106,9 +113,9 @@ access(all) contract MigrationContractStaging { /// Returns whether the Updater is properly configured /// NOTE: Does NOT check that the staged contract code is valid! /// - access(all) view fun verify(): Bool { - let checks: {String: String} = self.statusCheck() - for status in checks.values { + access(all) view fun isValid(): Bool { + let statuses: {String: String} = self.systemsCheck() + for status in statuses.values { if status != "PASSING" { return false } @@ -132,18 +139,18 @@ access(all) contract MigrationContractStaging { /// Platforms may utilize this method to display whether the Updater is properly configured in human-readable /// language /// - access(all) view fun statusCheck(): {String: String} { + access(all) view fun systemsCheck(): {String: String} { return { - "Updater": self.statusCheckUpdater(), - "Host": self.statusCheckHost(), - "Staged Update": self.statusCheckStagedUpdate(), - "Contract Existence": self.statusCheckTargetContractExists() + "Updater": self.checkUpdater(), + "Host": self.checkHost(), + "Staged Update": self.checkStagedUpdate(), + "Contract Existence": self.checkTargetContractExists() } } /// Returns the status of the Updater based on conditions relevant to the Cadence 1.0 contract migration /// - access(all) view fun statusCheckUpdater(): String { + access(all) view fun checkUpdater(): String { if self.owner == nil { return "FAILING: Unowned Updater" } else if self.owner!.address == self.host.getHostAddress() && @@ -160,8 +167,8 @@ access(all) contract MigrationContractStaging { /// Returns the status of the Host based on conditions relevant to the Cadence 1.0 contract migration /// - access(all) view fun statusCheckHost(): String { - if self.host.verify() { + access(all) view fun checkHost(): String { + if self.host.isValid() { return "PASSING" } else if self.host.checkAccountCapability() == false { return "FAILING: Account Capability check failed" @@ -176,7 +183,7 @@ access(all) contract MigrationContractStaging { /// Returns the status of the staged update based on conditions relevant to the Cadence 1.0 contract migration /// - access(all) view fun statusCheckStagedUpdate(): String { + access(all) view fun checkStagedUpdate(): String { if self.stagedUpdate.address != self.host.getHostAddress() { return "FAILING: Staged Update address does not match Host address" } else if self.owner == nil { @@ -191,8 +198,8 @@ access(all) contract MigrationContractStaging { /// Returns whether the staged contract exists on the target account. This is important as the contract /// migration only affects **existing** contracts /// - access(all) view fun statusCheckTargetContractExists(): String { - if self.stagedUpdate.verify() { + access(all) view fun checkTargetContractExists(): String { + if self.stagedUpdate.isValid() { return "PASSING" } else { return "FAILING: Target contract with name " diff --git a/scripts/migration-contract-staging/status_check.cdc b/scripts/migration-contract-staging/systems_check.cdc similarity index 93% rename from scripts/migration-contract-staging/status_check.cdc rename to scripts/migration-contract-staging/systems_check.cdc index fb70523..d68acda 100644 --- a/scripts/migration-contract-staging/status_check.cdc +++ b/scripts/migration-contract-staging/systems_check.cdc @@ -14,5 +14,5 @@ access(all) fun main(contractAddress: Address, contractName: String): {String: S let updaterPath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( contractAddress: contractAddress, contractName: contractName ) - return getAuthAccount(contractAddress).borrow<&MigrationContractStaging.Updater>(from: updaterPath)?.statusCheck() + return getAuthAccount(contractAddress).borrow<&MigrationContractStaging.Updater>(from: updaterPath)?.systemsCheck() } From 631df33f1247cfe0b8e3fd7b10aebb4d5a5e6acd Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:12:09 -0600 Subject: [PATCH 11/40] refactor MigrationContractStaging according to migration needs --- contracts/MigrationContractStaging.cdc | 436 ++++++++++++++----------- 1 file changed, 238 insertions(+), 198 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 8bef0c4..4637186 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -1,36 +1,180 @@ -/// This contract is intended for use for the Cadence 1.0 contract migration across the Flow network. +/// This contract is intended for use in the Cadence 1.0 contract migration across the Flow network. /// -/// To stage your contract for automated updates in preparation for Cadence 1.0, simply configure an Updater resource -/// in the contract account along with an AuthAccount Capability, the contract address, name and hex-encoded Cadence. +/// In preparation for this milestone, your contract will NEED to be updated! Once you've updated your code for +/// Cadence 1.0, you MUST stage your contract code in this contract so that the update can be executed as a part of the +/// network-wide state migration. /// -/// NOTE: Do not move the Updater resource from this account or unlink the Account Capability in your Host or you risk -/// breaking the automated update process for your contract. +/// To stage your contract update: +/// 1. create a Host & save in your contract-hosting account +/// 2. call stageContract() passing a reference to you Host, the contract name, and the hex-encoded Cadence code +/// +/// This can be done in a single transaction! For more code context, see https://github.com/onflow/contract-updater /// access(all) contract MigrationContractStaging { // Path derivation constants // - access(all) let delimiter: String - access(all) let updaterPathPrefix: String - access(all) let accountCapabilityPathPrefix: String - - /// Event emitted when an Updater is created - /// NOTE: Does not guarantee that the Updater is properly configured or even exists beyond event emission - access(all) event AccountContractStaged( - updaterUUID: UInt64, + access(self) let delimiter: String + access(self) let hostPathPrefix: String + access(self) let capsulePathPrefix: String + /// Maps contract addresses to an array of staged contract names + access(self) let stagedContracts: {Address: [String]} + + /// Event emitted when a contract's code is staged + /// status == true - insert | staged == false - replace | staged == nil - remove + /// NOTE: Does not guarantee that the contract code is valid Cadence + access(all) event StagingStatusUpdated( + capsuleUUID: UInt64, address: Address, codeHash: [UInt8], - contract: String + contract: String, + status: Bool? ) - /* --- ContractUpdate --- */ - // + /******************** + Public Methods + ********************/ + + /* --- Staging Methods --- */ + + /// 1 - Create a host and save it in your contract-hosting account at the path derived from deriveHostStoragePath() + /// + /// Creates a Host serving as identification for the contract account. Reference to this resource identifies the + /// calling address so it must be saved in account storage before being used to stage a contract update. + /// + access(all) fun createHost(): @Host { + return <- create Host() + } + + /// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage + /// + /// Stages the contract code for the given contract name at the host address. If the contract is already staged, + /// the code will be replaced. + access(all) fun stageContract(host: &Host, name: String, code: String) { + if self.stagedContracts[host.address()] == nil { + self.stagedContracts.insert(key: host.address(), [name]) + let capsule: @Capsule <- self.createCapsule(host: host, name: name, code: code) + + self.account.save(<-capsule, to: self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name)) + } else { + if let contractIndex: Int = self.stagedContracts[host.address()]!.firstIndex(of: name) { + self.stagedContracts[host.address()]!.remove(at: contractIndex) + } + self.stagedContracts[host.address()]!.append(name) + let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) + self.account.borrow<&Capsule>(from: capsulePath)!.replaceCode(code: code) + } + } + + /// Removes the staged contract code from the staging environment + /// + access(all) fun unstageContract(host: &Host, name: String) { + pre { + self.isStaged(address: host.address(), name: name): "Contract is not staged" + } + post { + !self.isStaged(address: host.address(), name: name): "Contract is still staged" + } + let address: Address = host.address() + let capsuleUUID: UInt64 = self.removeStagedContract(address: address, name: name) + ?? panic("Problem destroying update Capsule") + emit StagingStatusUpdated( + capsuleUUID: capsuleUUID, + address: address, + codeHash: [], + contract: name, + status: nil + ) + } + + /* --- Public Getters --- */ + + /// Returns true if the contract is currently staged + /// + access(all) fun isStaged(address: Address, name: String): Bool { + return self.stagedContracts[address]?.contains(name) ?? false + } + + /// Returns the names of all staged contracts for the given address + /// + access(all) fun getStagedContractNames(forAddress: Address): [String]? { + return self.stagedContracts[forAddress] + } + + /// Returns the staged contract Cadence code for the given address and name + /// + access(all) fun getStagedContractCode(address: Address, name: String): String? { + let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) + if let capsule = self.account.borrow<&Capsule>(from: capsulePath) { + return capsule.getContractUpdate().codeAsCadence() + } else { + return nil + } + } + + /// Returns an array of all staged contract host addresses + /// + access(all) view fun getAllStagedContractHosts(): [Address] { + return self.stagedContracts.keys + } + + /// Returns a dictionary of all staged contract code for the given address + /// + access(all) fun getAllStagedContractCode(forAddress: Address): {String: String} { + if self.stagedContracts[forAddress] == nil { + return {} + } + let capsulePaths: [StoragePath] = [] + let stagedCode: {String: String} = {} + let contractNames: [String] = self.stagedContracts[forAddress]! + for name in contractNames { + capsulePaths.append(self.deriveCapsuleStoragePath(contractAddress: forAddress, contractName: name)) + } + for path in capsulePaths { + if let capsule = self.account.borrow<&Capsule>(from: path) { + let update: ContractUpdate = capsule.getContractUpdate() + stagedCode[update.name] = update.codeAsCadence() + } + } + return stagedCode + } + + /// Returns a StoragePath to store the Host of the form: + /// /storage/self.hostPathPrefix_ADDRESS + access(all) fun deriveHostStoragePath(hostAddress: Address): StoragePath { + return StoragePath( + identifier: self.hostPathPrefix + .concat(self.delimiter) + .concat(hostAddress.toString()) + ) ?? panic("Could not derive Host StoragePath for given address") + } + + /// Returns a StoragePath to store the Capsule of the form: + /// /storage/self.capsulePathPrefix_ADDRESS_NAME + access(all) fun deriveCapsuleStoragePath(contractAddress: Address, contractName: String): StoragePath { + return StoragePath( + identifier: self.capsulePathPrefix + .concat(self.delimiter) + .concat(contractAddress.toString()) + .concat(self.delimiter) + .concat(contractName) + ) ?? panic("Could not derive Capsule StoragePath for given address") + } + + /* ------------------------------------------------------------------------------------------------------------ */ + /* ------------------------------------------------ Constructs ------------------------------------------------ */ + /* ------------------------------------------------------------------------------------------------------------ */ + + /******************** + ContractUpdate + ********************/ + /// Represents contract and its corresponding code /// access(all) struct ContractUpdate { access(all) let address: Address access(all) let name: String - access(all) let code: String + access(all) var code: String init(address: Address, name: String, code: String) { self.address = address @@ -55,224 +199,120 @@ access(all) contract MigrationContractStaging { access(all) view fun codeAsCadence(): String { return String.fromUTF8(self.code.decodeHex()) ?? panic("Problem stringifying code!") } - } - - /* --- Host --- */ - // - /// Encapsulates an AuthAccount, exposing only the ability to update contracts on the underlying account - /// - access(all) resource Host { - /// Capability on the underlying account, possession of which serves as proof of access on the account - access(self) let accountCapability: Capability<&AuthAccount> - - init(accountCapability: Capability<&AuthAccount>) { - self.accountCapability = accountCapability - } - /// Verifies that the encapsulated Account is the owner of this Host - /// - access(all) view fun isValid(): Bool { - return self.getHostAddress() != nil && self.getHostAddress() == self.owner?.address + access(contract) fun replaceCode(_ code: String) { + self.code = code } + } - /// Checks the wrapped AuthAccount Capability - /// - access(all) view fun checkAccountCapability(): Bool { - return self.accountCapability.check() - } + /******************** + Host + ********************/ - /// Returns the Address of the underlying account + /// Serves as identification for a caller's address. + /// NOTE: Should be saved in storage and access safeguarded as reference grants access to contract staging. + /// + access(all) resource Host { + /// Returns the resource owner's address /// - access(all) view fun getHostAddress(): Address? { - return self.accountCapability.borrow()?.address + access(all) view fun address(): Address{ + return self.owner?.address ?? panic("Host is unowned!") } } - /* --- Updater --- */ - // - /// Resource that enables staged contract updates to the Host account. In the context of the Cadence 1.0 migration, - /// this Updater should be stored with a ContractUpdate in the account which is to be updated. An Updater should be - /// configured for each contract in an account. + /******************** + Capsule + ********************/ + + /// Resource that stores pending contract updates in a ContractUpdate struct. On staging a contract update for the + /// first time, a Capsule will be created and stored in this contract account. Any time a stageContract() call is + /// made again for the same contract, the code in the Capsule will be replaced. As you see, the Capsule is merely + /// intended to store the code, as contract updates will be executed by state migration across the network at the + /// Cadence 1.0 milestone. /// - access(all) resource Updater { - /// The Host resource encapsulating the Account Capability - access(self) let host: @Host + access(all) resource Capsule { /// The address, name and code of the contract that will be updated - access(self) let stagedUpdate: ContractUpdate + access(self) let update: ContractUpdate - init(host: @Host, stagedUpdate: ContractUpdate) { + init(update: ContractUpdate) { pre { - host.getHostAddress() == stagedUpdate.address: "Host and update address must match" - stagedUpdate.codeAsCadence() != nil: "Staged update code must be valid Cadence" - stagedUpdate.isValid(): "Target contract does not exist" + update.codeAsCadence() != nil: "Staged update code must be valid Cadence" + update.isValid(): "Target contract does not exist" } - self.host <- host - self.stagedUpdate = stagedUpdate - } - - /// Returns whether the Updater is properly configured - /// NOTE: Does NOT check that the staged contract code is valid! - /// - access(all) view fun isValid(): Bool { - let statuses: {String: String} = self.systemsCheck() - for status in statuses.values { - if status != "PASSING" { - return false - } - } - return true - } - - /// Enables borrowing of a Host reference. Since all Host methods are view, this is safe to do. - /// - access(all) view fun borrowHost(): &Host { - return &self.host as &Host + self.update = update } /// Returns the staged contract update in the form of a ContractUpdate struct /// access(all) view fun getContractUpdate(): ContractUpdate { - return self.stagedUpdate - } - - /// Checks all conditions **EXCEPT CODE VALIDITY** and reports back with a map of statuses for each check - /// Platforms may utilize this method to display whether the Updater is properly configured in human-readable - /// language - /// - access(all) view fun systemsCheck(): {String: String} { - return { - "Updater": self.checkUpdater(), - "Host": self.checkHost(), - "Staged Update": self.checkStagedUpdate(), - "Contract Existence": self.checkTargetContractExists() - } - } - - /// Returns the status of the Updater based on conditions relevant to the Cadence 1.0 contract migration - /// - access(all) view fun checkUpdater(): String { - if self.owner == nil { - return "FAILING: Unowned Updater" - } else if self.owner!.address == self.host.getHostAddress() && - self.owner!.address == self.stagedUpdate.address { - return "PASSING" - } else if self.owner!.address != self.stagedUpdate.address { - return "FAILING: Owner address does not match staged Update target address" - } else if self.owner!.address != self.host.getHostAddress() { - return "FAILING: Owner address does not match Host address" - } else { - return "FAILING: Unknown error" - } + return self.update } - /// Returns the status of the Host based on conditions relevant to the Cadence 1.0 contract migration + /// Replaces the staged contract code with the given hex-encoded Cadence code /// - access(all) view fun checkHost(): String { - if self.host.isValid() { - return "PASSING" - } else if self.host.checkAccountCapability() == false { - return "FAILING: Account Capability check failed" - } else if self.owner == nil { - return "FAILING: Unowned Updater" - } else if self.host.getHostAddress()! != self.owner!.address { - return "FAILING: Host address does not match owner address" - } else { - return "FAILING: Unknown error" + access(contract) fun replaceCode(code: String) { + post { + self.update.codeAsCadence() != nil: "Staged update code must be valid Cadence" } - } - - /// Returns the status of the staged update based on conditions relevant to the Cadence 1.0 contract migration - /// - access(all) view fun checkStagedUpdate(): String { - if self.stagedUpdate.address != self.host.getHostAddress() { - return "FAILING: Staged Update address does not match Host address" - } else if self.owner == nil { - return "FAILING: Unowned Updater" - } else if self.stagedUpdate.address == self.host.getHostAddress() { - return "PASSING" - } else { - return "FAILING: Unknown error" - } - } - - /// Returns whether the staged contract exists on the target account. This is important as the contract - /// migration only affects **existing** contracts - /// - access(all) view fun checkTargetContractExists(): String { - if self.stagedUpdate.isValid() { - return "PASSING" - } else { - return "FAILING: Target contract with name " - .concat(self.stagedUpdate.name) - .concat(" does not exist at address ") - .concat(self.stagedUpdate.address.toString()) - } - } - - destroy() { - destroy self.host + self.update.replaceCode(code) + emit StagingStatusUpdated( + capsuleUUID: self.uuid, + address: self.update.address, + codeHash: code.decodeHex(), + contract: self.update.name, + status: false + ) } } - /// Returns a StoragePath to store the Updater of the form: - /// /storage/self.updaterPathPrefix_ADDRESS_NAME - access(all) fun deriveUpdaterStoragePath(contractAddress: Address, contractName: String): StoragePath { - return StoragePath( - identifier: self.updaterPathPrefix - .concat(self.delimiter) - .concat(contractAddress.toString()) - .concat(self.delimiter) - .concat(contractName) - ) ?? panic("Could not derive Updater StoragePath for given address") - } - - /// Returns a PublicPath to store the Updater of the form: - /// /storage/self.updaterPathPrefix_ADDRESS_NAME - access(all) fun deriveUpdaterPublicPath(contractAddress: Address, contractName: String): PublicPath { - return PublicPath( - identifier: self.updaterPathPrefix - .concat(self.delimiter) - .concat(contractAddress.toString()) - .concat(self.delimiter) - .concat(contractName) - ) ?? panic("Could not derive Updater PublicPath for given address") - } + /********************* + Internal Methods + *********************/ - /// Returns a PrivatePath to store the Account Capability of the form: - /// /storage/self.accountCapabilityPathPrefix_ADDRESS - access(all) fun deriveAccountCapabilityPath(forAddress: Address): PrivatePath { - return PrivatePath( - identifier: self.accountCapabilityPathPrefix.concat(self.delimiter).concat(forAddress.toString()) - ) ?? panic("Could not derive Account Capability path for given address") + /// Creates a Capsule resource with the given Host and ContractUpdate. Will be stored at the derived path in this + /// contract's account storage. + /// + access(self) fun createCapsule(host: &Host, name: String, code: String): @Capsule { + let update = ContractUpdate(address: host.address(), name: name, code: code) + let capsule <- create Capsule(update: update) + emit StagingStatusUpdated( + capsuleUUID: capsule.uuid, + address: host.address(), + codeHash: code.decodeHex(), + contract: name, + status: true + ) + return <- capsule } - /// Creates a Host resource wrapping the given account capability - /// - access(all) fun createHost(accountCapability: Capability<&AuthAccount>): @Host { - return <- create Host(accountCapability: accountCapability) + access(self) fun removeStagedContract(address: Address, name: String): UInt64? { + let contractIndex: Int = self.stagedContracts[address]!.firstIndex(of: name)! + self.stagedContracts[address]!.remove(at: contractIndex) + // Remove the Address from the stagedContracts mapping if it has no staged contracts remain for the host address + if self.stagedContracts[address]!.length == 0 { + self.stagedContracts.remove(key: address) + } + return self.destroyCapsule(address: address, name: name) } - /// Creates an Updater resource with the given Host and ContractUpdate. Should be stored at the derived path in the - /// target address - the same account the Host maintains an Account Capability for. - /// - access(all) fun createUpdater(host: @Host, stagedUpdate: ContractUpdate): @Updater { - let updater: @MigrationContractStaging.Updater <- create Updater(host: <-host, stagedUpdate: stagedUpdate) - emit AccountContractStaged( - updaterUUID: updater.uuid, - address: stagedUpdate.address, - codeHash: stagedUpdate.code.decodeHex(), - contract: stagedUpdate.name - ) - return <- updater + access(self) fun destroyCapsule(address: Address, name: String): UInt64? { + let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) + if let capsule <- self.account.load<@Capsule>(from: capsulePath) { + let capsuleUUID = capsule.uuid + destroy capsule + return capsuleUUID + } + return nil } init() { self.delimiter = "_" - self.accountCapabilityPathPrefix = "MigrationContractStagingHostAccountCapability" + self.hostPathPrefix = "MigrationContractStagingHost".concat(self.delimiter) .concat(self.delimiter) .concat(self.account.address.toString()) - self.updaterPathPrefix = "MigrationContractStagingUpdater" + self.capsulePathPrefix = "MigrationContractStagingCapsule" .concat(self.delimiter) .concat(self.account.address.toString()) + self.stagedContracts = {} } } From d69f40ed569e1a6e1e7cfb7c589cbc293deddafe Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:23:59 -0600 Subject: [PATCH 12/40] refactor migration transactions & scripts --- .../get_staged_contract_code.cdc | 7 +-- .../systems_check.cdc | 18 ------ .../stage_contract.cdc | 60 ++++++------------- .../unstage_contract.cdc | 31 ++++++---- 4 files changed, 37 insertions(+), 79 deletions(-) delete mode 100644 scripts/migration-contract-staging/systems_check.cdc diff --git a/scripts/migration-contract-staging/get_staged_contract_code.cdc b/scripts/migration-contract-staging/get_staged_contract_code.cdc index b9bd4bc..6394b20 100644 --- a/scripts/migration-contract-staging/get_staged_contract_code.cdc +++ b/scripts/migration-contract-staging/get_staged_contract_code.cdc @@ -3,10 +3,5 @@ import "MigrationContractStaging" /// Returns the code as it is staged or nil if it not currently staged. /// access(all) fun main(contractAddress: Address, contractName: String): String? { - let updaterPath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( - contractAddress: contractAddress, contractName: contractName - ) - return getAuthAccount(contractAddress).borrow<&MigrationContractStaging.Updater>(from: updaterPath) - ?.getContractUpdate() - ?.codeAsCadence() + return MigrationContractStaging.getStagedContractCode(address: contractAddress, name: contractName) } diff --git a/scripts/migration-contract-staging/systems_check.cdc b/scripts/migration-contract-staging/systems_check.cdc deleted file mode 100644 index d68acda..0000000 --- a/scripts/migration-contract-staging/systems_check.cdc +++ /dev/null @@ -1,18 +0,0 @@ -import "MigrationContractStaging" - -/// Returns a mapping of status checks on various conditions affecting contract staging or nil if not staged. -/// A successful status check will look like: -/// { -/// "Updater": "PASSING", -/// "Host": "PASSING", -/// "Staged Update": "PASSING", -/// "Contract Existence": "PASSING" -/// } -/// If a status check fails, the value will be "FAILING:" followed by the reason for failure -/// -access(all) fun main(contractAddress: Address, contractName: String): {String: String}? { - let updaterPath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( - contractAddress: contractAddress, contractName: contractName - ) - return getAuthAccount(contractAddress).borrow<&MigrationContractStaging.Updater>(from: updaterPath)?.systemsCheck() -} diff --git a/transactions/migration-contract-staging/stage_contract.cdc b/transactions/migration-contract-staging/stage_contract.cdc index 363faad..a6130fc 100644 --- a/transactions/migration-contract-staging/stage_contract.cdc +++ b/transactions/migration-contract-staging/stage_contract.cdc @@ -1,8 +1,7 @@ -#allowAccountLinking - import "MigrationContractStaging" /// This transaction is used to stage a contract update for Cadence 1.0 contract migrations. +/// /// Ensure that this transaction is signed by the account that owns the contract to be updated and that the contract /// has already been deployed to the signing account. /// @@ -12,48 +11,25 @@ import "MigrationContractStaging" /// @param contractCode: The updated contract code as a hex-encoded String /// transaction(contractName: String, contractCode: String) { - let accountCapability: Capability<&AuthAccount> + let host: &MigrationContractStaging.Host prepare(signer: AuthAccount) { - // Retrieve an AuthAccount Capability to the signer's account - let accountCapabilityPath: PrivatePath = MigrationContractStaging.deriveAccountCapabilityPath( - forAddress: signer.address - ) - if signer.getCapability<&AuthAccount>(accountCapabilityPath).borrow() == nil { - self.accountCapability = signer.linkAccount(accountCapabilityPath) ?? panic("Problem linking account") - } else { - self.accountCapability = signer.getCapability<&AuthAccount>(accountCapabilityPath) + // Configure Host resource if needed + let hostStoragePath: StoragePath = MigrationContractStaging.deriveHostStoragePath(hostAddress: signer.address) + if signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath) == nil { + signer.save(<-MigrationContractStaging.createHost(), to: hostStoragePath) } - // Create a Host resource, wrapping the retrieved Account Capability - let host: @MigrationContractStaging.Host <- MigrationContractStaging.createHost( - accountCapability: self.accountCapability - ) - - // Create an Updater resource, staging the update - let updaterStoragePath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( - contractAddress: signer.address, - contractName: contractName - ) - // Ensure that an Updater resource doesn't already exist for this contract. If so, revert. Signer should - // inspect the existing Updater and destroy if needed before re-attempting this transaction - assert( - signer.borrow<&MigrationContractStaging.Updater>(from: updaterStoragePath) == nil, - message: "Updater already exists" - ) - signer.save( - <-MigrationContractStaging.createUpdater( - host: <-host, - stagedUpdate: MigrationContractStaging.ContractUpdate( - address: signer.address, - name: contractName, - code: contractCode - ) - ), to: updaterStoragePath - ) - let updaterPublicPath: PublicPath = MigrationContractStaging.deriveUpdaterPublicPath( - contractAddress: signer.address, - contractName: contractName - ) - signer.link<&MigrationContractStaging.Updater>(updaterPublicPath, target: updaterStoragePath) + // Assign Host reference + self.host = signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath)! + } + + execute { + // Call staging contract, storing the contract code that will update during Cadence 1.0 migration + MigrationContractStaging.stageContract(host: self.host, name: contractName, code: contractCode) + } + + post { + MigrationContractStaging.isStaged(address: self.host.address(), name: contractName): + "Problem while staging update" } } diff --git a/transactions/migration-contract-staging/unstage_contract.cdc b/transactions/migration-contract-staging/unstage_contract.cdc index 0b9ea44..d7d2fda 100644 --- a/transactions/migration-contract-staging/unstage_contract.cdc +++ b/transactions/migration-contract-staging/unstage_contract.cdc @@ -1,22 +1,27 @@ import "MigrationContractStaging" -/// Loads and destroys the existing updater for the given contract name in the signer's account if exists. This means -/// the contract will no longer be staged for migrated updates. +/// Unstages the given contract from the staging contract. Only the contract host can perform this action. +/// After the transaction, the contract will no longer be staged for Cadence 1.0 migration. /// /// For more context, see the repo - https://github.com/onflow/contract-updater /// transaction(contractName: String) { - + let host: &MigrationContractStaging.Host + prepare(signer: AuthAccount) { - let updaterStoragePath: StoragePath = MigrationContractStaging.deriveUpdaterStoragePath( - contractAddress: signer.address, - contractName: contractName - ) - let updaterPublicPath: PublicPath = MigrationContractStaging.deriveUpdaterPublicPath( - contractAddress: signer.address, - contractName: contractName - ) - signer.unlink(updaterPublicPath) - destroy signer.load<@MigrationContractStaging.Updater>(from: updaterStoragePath) + // Assign Host reference + let hostStoragePath: StoragePath = MigrationContractStaging.deriveHostStoragePath(hostAddress: signer.address) + self.host = signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath) + ?? panic("Host was not found in storage") + } + + execute { + // Call staging contract, storing the contract code that will update during Cadence 1.0 migration + MigrationContractStaging.unstageContract(host: self.host, name: contractName) + } + + post { + MigrationContractStaging.isStaged(address: self.host.address(), name: contractName) == false: + "Problem while unstaging update" } } From 7a31e2054ee7787a65ba68a91f1709763dc64aad Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:31:50 -0600 Subject: [PATCH 13/40] add helper scripts & update comments --- .../get_all_staged_contract_code_for_address.cdc | 7 +++++++ .../get_all_staged_contract_hosts.cdc | 7 +++++++ .../get_staged_contract_names_for_address.cdc | 7 +++++++ scripts/migration-contract-staging/is_staged.cdc | 7 +++++++ transactions/migration-contract-staging/stage_contract.cdc | 1 + 5 files changed, 29 insertions(+) create mode 100644 scripts/migration-contract-staging/get_all_staged_contract_code_for_address.cdc create mode 100644 scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc create mode 100644 scripts/migration-contract-staging/get_staged_contract_names_for_address.cdc create mode 100644 scripts/migration-contract-staging/is_staged.cdc diff --git a/scripts/migration-contract-staging/get_all_staged_contract_code_for_address.cdc b/scripts/migration-contract-staging/get_all_staged_contract_code_for_address.cdc new file mode 100644 index 0000000..8e74e89 --- /dev/null +++ b/scripts/migration-contract-staging/get_all_staged_contract_code_for_address.cdc @@ -0,0 +1,7 @@ +import "MigrationContractStaging" + +/// Returns the code for all staged contracts hosted by the given contract address. +/// +access(all) fun main(contractAddress: Address): {String: String} { + return MigrationContractStaging.getAllStagedContractCode(forAddress: contractAddress) +} diff --git a/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc b/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc new file mode 100644 index 0000000..17949e4 --- /dev/null +++ b/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc @@ -0,0 +1,7 @@ +import "MigrationContractStaging" + +/// Returns the code for all staged contracts hosted by the given contract address. +/// +access(all) fun main(contractAddress: Address): [Address] { + return MigrationContractStaging.getAllStagedContractHosts() +} diff --git a/scripts/migration-contract-staging/get_staged_contract_names_for_address.cdc b/scripts/migration-contract-staging/get_staged_contract_names_for_address.cdc new file mode 100644 index 0000000..167651d --- /dev/null +++ b/scripts/migration-contract-staging/get_staged_contract_names_for_address.cdc @@ -0,0 +1,7 @@ +import "MigrationContractStaging" + +/// Returns the names of all contracts staged by a certain address +/// +access(all) fun main(contractAddress: Address): [String] { + return MigrationContractStaging.getStagedContractNames(forAddress: contractAddress) +} diff --git a/scripts/migration-contract-staging/is_staged.cdc b/scripts/migration-contract-staging/is_staged.cdc new file mode 100644 index 0000000..8306de9 --- /dev/null +++ b/scripts/migration-contract-staging/is_staged.cdc @@ -0,0 +1,7 @@ +import "MigrationContractStaging" + +/// Returns whether the given contract is staged or not +/// +access(all) fun main(contractAddress: Address, contractName: String): Bool { + return MigrationContractStaging.isStaged(address: contractAddress, name: contractName) +} diff --git a/transactions/migration-contract-staging/stage_contract.cdc b/transactions/migration-contract-staging/stage_contract.cdc index a6130fc..222e4e8 100644 --- a/transactions/migration-contract-staging/stage_contract.cdc +++ b/transactions/migration-contract-staging/stage_contract.cdc @@ -25,6 +25,7 @@ transaction(contractName: String, contractCode: String) { execute { // Call staging contract, storing the contract code that will update during Cadence 1.0 migration + // If code is already staged for the given contract, it will be overwritten. MigrationContractStaging.stageContract(host: self.host, name: contractName, code: contractCode) } From 3d850a2eb9da643170fd8ec79f1df1db25eaf0df Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:00:36 -0600 Subject: [PATCH 14/40] update MigrationContractStaging.getStagedContractNames() return type --- contracts/MigrationContractStaging.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 4637186..131bedf 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -97,8 +97,8 @@ access(all) contract MigrationContractStaging { /// Returns the names of all staged contracts for the given address /// - access(all) fun getStagedContractNames(forAddress: Address): [String]? { - return self.stagedContracts[forAddress] + access(all) fun getStagedContractNames(forAddress: Address): [String] { + return self.stagedContracts[forAddress] ?? [] } /// Returns the staged contract Cadence code for the given address and name From f0fe3a642f85191c31c836727c79de464d22e04b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:16:15 -0600 Subject: [PATCH 15/40] revert StagedContractUpdates related changes --- contracts/StagedContractUpdates.cdc | 99 ++++--------------- ...ntract_delegatee_block_update_boundary.cdc | 5 - tests/staged_contract_updater_tests.cdc | 66 +++++-------- .../coordinator/set_block_update_boundary.cdc | 17 +--- .../delegatee/set_block_update_boundary.cdc | 19 ---- ...up_updater_single_account_and_contract.cdc | 6 +- 6 files changed, 49 insertions(+), 163 deletions(-) delete mode 100644 scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc delete mode 100644 transactions/delegatee/set_block_update_boundary.cdc diff --git a/contracts/StagedContractUpdates.cdc b/contracts/StagedContractUpdates.cdc index 174d958..1e69c2c 100644 --- a/contracts/StagedContractUpdates.cdc +++ b/contracts/StagedContractUpdates.cdc @@ -1,9 +1,5 @@ import "MetadataViews" -/******************************************************************************************************************* - NOTICE: THIS CONTRACT IS NOT INTENDED FOR CADENCE 1.0 STAGING - SEE `MigrationContractStaging` FOR 1.0 STAGING - *******************************************************************************************************************/ - /// This contract defines resources which enable storage of contract code for the purposes of updating at or beyond /// some blockheight boundary either by the containing resource's owner or by some delegated party. /// @@ -22,7 +18,7 @@ access(all) contract StagedContractUpdates { /// Common update boundary for those coordinating with contract account-managed Delegatee, enabling opt-in /// Flow coordinated contract updates - access(all) var blockUpdateBoundary: UInt64? + access(all) var blockUpdateBoundary: UInt64 /* --- Canonical Paths --- */ // @@ -38,7 +34,7 @@ access(all) contract StagedContractUpdates { /// Event emitted when the contract block update boundary is updated access(all) event ContractBlockUpdateBoundaryUpdated(old: UInt64?, new: UInt64) /// Event emitted when an Updater is created - access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64, stagedContracts: [String]) + access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64) /// Event emitted when an Updater is updated access(all) event UpdaterUpdated( updaterUUID: UInt64, @@ -51,10 +47,7 @@ access(all) contract StagedContractUpdates { failedContracts: [String], updateComplete: Bool ) - /// Represents a change in delegation status for an Updater - delegated: added to Delegatee, !delegated: removed access(all) event UpdaterDelegationChanged(updaterUUID: UInt64, updaterAddress: Address?, delegated: Bool) - /// Event emitted when a Delegatee's block update boundary is set, signifying the Delegatee is ready for delegation - access(all) event DelegateeBlockUpdateBoundarySet(delegateeUUID: UInt64, address: Address?, blockHeight: UInt64) /// Represents contract and its corresponding code /// @@ -167,7 +160,7 @@ access(all) contract StagedContractUpdates { /// access(all) resource Updater : UpdaterPublic, MetadataViews.Resolver { /// Update to occur at or beyond this block height - access(self) var blockUpdateBoundary: UInt64 + access(self) let blockUpdateBoundary: UInt64 /// Update status defining whether all update stages have been *attempted* /// NOTE: `true` does not necessarily mean all updates were successful access(self) var updateComplete: Bool @@ -331,12 +324,6 @@ access(all) contract StagedContractUpdates { } return nil } - - /* --- Delegatee access override --- */ - - access(contract) fun delegateeBoundaryOverride(_ newBoundary: UInt64) { - self.blockUpdateBoundary = newBoundary - } } /* --- Delegatee --- */ @@ -345,7 +332,6 @@ access(all) contract StagedContractUpdates { /// access(all) resource interface DelegateePublic { access(all) fun check(id: UInt64): Bool? - access(all) fun getBlockUpdateBoundary(): UInt64? access(all) fun getUpdaterIDs(): [UInt64] access(all) fun delegate(updaterCap: Capability<&Updater>) access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>) @@ -357,25 +343,13 @@ access(all) contract StagedContractUpdates { /// Block height at which delegated updates will be performed by this Delegatee /// NOTE: This may differ from the contract's blockUpdateBoundary, enabling flexibility but any Updaters not /// ready when updates are performed will be revoked from the Delegatee - access(self) var blockUpdateBoundary: UInt64? + access(self) let blockUpdateBoundary: UInt64 /// Mapping of all delegated Updater Capabilities by their UUID access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>} - init(blockUpdateBoundary: UInt64?) { - pre { - blockUpdateBoundary == nil || blockUpdateBoundary! > getCurrentBlock().height: - "Block update boundary must be in the future!" - } + init(blockUpdateBoundary: UInt64) { self.blockUpdateBoundary = blockUpdateBoundary self.delegatedUpdaters = {} - - if blockUpdateBoundary != nil { - emit DelegateeBlockUpdateBoundarySet( - delegateeUUID: self.uuid, - address: nil, - blockHeight: blockUpdateBoundary! - ) - } } /// Checks if the specified DelegatedUpdater Capability is contained and valid @@ -384,12 +358,6 @@ access(all) contract StagedContractUpdates { return self.delegatedUpdaters[id]?.check() ?? nil } - /// Getter for block update boundary - /// - access(all) fun getBlockUpdateBoundary(): UInt64? { - return self.blockUpdateBoundary - } - /// Returns the IDs of delegated Updaters /// access(all) fun getUpdaterIDs(): [UInt64] { @@ -400,19 +368,15 @@ access(all) contract StagedContractUpdates { /// access(all) fun delegate(updaterCap: Capability<&Updater>) { pre { - self.blockUpdateBoundary != nil: - "Delegation is not yet enabled, wait for Delegatee to set block update boundary" - getCurrentBlock().height < self.blockUpdateBoundary!: - "Delegation must occur before Delegatee boundary of ".concat(self.blockUpdateBoundary!.toString()) + getCurrentBlock().height < self.blockUpdateBoundary: + "Delegation must occur before Delegatee boundary of ".concat(self.blockUpdateBoundary.toString()) updaterCap.check(): "Invalid DelegatedUpdater Capability!" updaterCap.borrow()!.hasBeenUpdated() == false: "Updater has already been updated!" - updaterCap.borrow()!.getBlockUpdateBoundary() <= self.blockUpdateBoundary!: - "Updater will not be ready for updates at Delegatee boundary of ".concat(self.blockUpdateBoundary!.toString()) + updaterCap.borrow()!.getBlockUpdateBoundary() <= self.blockUpdateBoundary: + "Updater will not be ready for updates at Delegatee boundary of ".concat(self.blockUpdateBoundary.toString()) } - let updater: &StagedContractUpdates.Updater = updaterCap.borrow()! - - updater.delegateeBoundaryOverride(self.blockUpdateBoundary!) + let updater: &StagedContractUpdates.Updater = updaterCap.borrow()! if self.delegatedUpdaters.containsKey(updater.getID()) { // Upsert if updater Capability already contained self.delegatedUpdaters[updater.getID()] = updaterCap @@ -435,21 +399,6 @@ access(all) contract StagedContractUpdates { self.removeDelegatedUpdater(id: updater.getID()) } - /// Enables the Delegatee to set the block update boundary if it wasn't set on init, enabling delegation - /// - access(all) fun setBlockUpdateBoundary(blockHeight: UInt64) { - pre { - self.blockUpdateBoundary == nil: "Update boundary has already been set" - blockHeight > getCurrentBlock().height: "Block update boundary must be in the future!" - } - self.blockUpdateBoundary = blockHeight - emit DelegateeBlockUpdateBoundarySet( - delegateeUUID: self.uuid, - address: self.owner!.address, - blockHeight: blockHeight - ) - } - /// Executes update on the specified Updaters. All updates are attempted, and if the Updater is not yet ready /// to be updated (updater.update() returns nil) or the attempted update is the final staged (updater.update() /// returns true), the corresponding Updater Capability is removed. @@ -471,7 +420,6 @@ access(all) contract StagedContractUpdates { // Execute currently staged update let success: Bool? = updaterCap.borrow()!.update() // If update is not ready or complete, remove Capability and continue - // NOTE: Checked to prevent revert in case the Updater resource our Capability targets was swapped if success == nil || success! == true { self.delegatedUpdaters.remove(key: id) continue @@ -504,10 +452,9 @@ access(all) contract StagedContractUpdates { access(all) fun setBlockUpdateBoundary(new: UInt64) { pre { new > getCurrentBlock().height: "New boundary must be in the future!" - StagedContractUpdates.blockUpdateBoundary == nil || new > StagedContractUpdates.blockUpdateBoundary!: - "New block update boundary must be greater than current boundary!" + new > StagedContractUpdates.blockUpdateBoundary: "New block update boundary must be greater than current boundary!" } - let old: UInt64? = StagedContractUpdates.blockUpdateBoundary + let old = StagedContractUpdates.blockUpdateBoundary StagedContractUpdates.blockUpdateBoundary = new emit ContractBlockUpdateBoundaryUpdated(old: old, new: new) } @@ -519,7 +466,7 @@ access(all) contract StagedContractUpdates { /// access(all) fun getContractDelegateeCapability(): Capability<&{DelegateePublic}> { let delegateeCap = self.account.getCapability<&{DelegateePublic}>(self.DelegateePublicPath) - assert(delegateeCap.check(), message: "Invalid Delegatee Capability retrieved from contract account") + assert(delegateeCap.check(), message: "Invalid Delegatee Capability retrieved") return delegateeCap } @@ -571,27 +518,21 @@ access(all) contract StagedContractUpdates { deployments: [[ContractUpdate]] ): @Updater { let updater <- create Updater(blockUpdateBoundary: blockUpdateBoundary, hosts: hosts, deployments: deployments) - let contracts: [String] = [] - for deployment in deployments { - for update in deployment { - contracts.append(update.toString()) - } - } - emit UpdaterCreated(updaterUUID: updater.uuid, blockUpdateBoundary: blockUpdateBoundary, stagedContracts: contracts) + emit UpdaterCreated(updaterUUID: updater.uuid, blockUpdateBoundary: blockUpdateBoundary) return <- updater } /// Creates a new Delegatee resource enabling caller to self-host their Delegatee to be executed at or beyond /// given block update boundary /// - access(all) fun createNewDelegatee(blockUpdateBoundary: UInt64?): @Delegatee { + access(all) fun createNewDelegatee(blockUpdateBoundary: UInt64): @Delegatee { return <- create Delegatee(blockUpdateBoundary: blockUpdateBoundary) } - init() { + init(blockUpdateBoundary: UInt64) { let contractAddress = self.account.address.toString() - self.blockUpdateBoundary = nil + self.blockUpdateBoundary = blockUpdateBoundary self.inboxHostCapabilityNamePrefix = "StagedContractUpdatesHostCapability_" self.HostStoragePath = StoragePath(identifier: "StagedContractUpdatesHost_".concat(contractAddress))! @@ -601,9 +542,11 @@ access(all) contract StagedContractUpdates { self.DelegateePublicPath = PublicPath(identifier: "StagedContractUpdatesDelegateePublic_".concat(contractAddress))! self.CoordinatorStoragePath = StoragePath(identifier: "StagedContractUpdatesCoordinator_".concat(contractAddress))! - self.account.save(<-create Delegatee(blockUpdateBoundary: nil), to: self.DelegateeStoragePath) + self.account.save(<-create Delegatee(blockUpdateBoundary: blockUpdateBoundary), to: self.DelegateeStoragePath) self.account.link<&{DelegateePublic}>(self.DelegateePublicPath, target: self.DelegateeStoragePath) self.account.save(<-create Coordinator(), to: self.CoordinatorStoragePath) + + emit ContractBlockUpdateBoundaryUpdated(old: nil, new: blockUpdateBoundary) } -} +} diff --git a/scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc b/scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc deleted file mode 100644 index 5deb38c..0000000 --- a/scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc +++ /dev/null @@ -1,5 +0,0 @@ -import "StagedContractUpdates" - -access(all) fun main(): UInt64? { - return StagedContractUpdates.getContractDelegateeCapability().borrow()?.getBlockUpdateBoundary() ?? nil -} diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index c4ed5a0..3e68b99 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -23,7 +23,7 @@ access(all) fun setup() { var err = Test.deployContract( name: "StagedContractUpdates", path: "../contracts/StagedContractUpdates.cdc", - arguments: [] + arguments: [getCurrentBlockHeight() + blockHeightBoundaryDelay] ) Test.expect(err, Test.beNil()) @@ -56,45 +56,6 @@ access(all) fun setup() { Test.expect(err, Test.beNil()) } -access(all) fun testCoordinatorSetBlockUpdateBoundaryFails() { - let txResult = executeTransaction( - "../transactions/coordinator/set_block_update_boundary.cdc", - [1], - admin - ) - Test.expect(txResult, Test.beFailed()) -} - -access(all) fun testCoordinatorSetBlockUpdateBoundarySucceeds() { - let expectedBoundary = getCurrentBlockHeight() + blockHeightBoundaryDelay - let txResult = executeTransaction( - "../transactions/coordinator/set_block_update_boundary.cdc", - [expectedBoundary], - admin - ) - Test.expect(txResult, Test.beSucceeded()) - - // TODO: Uncomment once bug is fixed allowing contract import - // let actualBoundary = StagedContractUpdates.blockUpdateBoundary - // Test.assertEqual(expectedBoundary, actualBoundary) - // events = Test.eventsOfType(Type()) - // Test.assertEqual(1, events.length) -} - -access(all) fun testDelegateeSetBlockUpdateBoundarySucceeds() { - let expectedBoundary = getCurrentBlockHeight() + blockHeightBoundaryDelay - let txResult = executeTransaction( - "../transactions/delegatee/set_block_update_boundary.cdc", - [expectedBoundary], - admin - ) - Test.expect(txResult, Test.beSucceeded()) - - let actualBoundary = executeScript("../scripts/delegatee/get_contract_delegatee_block_update_boundary.cdc", []).returnValue as! UInt64? - ?? panic("Problem retrieving block update boundary") - Test.assertEqual(expectedBoundary, actualBoundary) -} - access(all) fun testEmptyDeploymentUpdaterInitFails() { let alice = Test.createAccount() let txResult = executeTransaction( @@ -105,7 +66,7 @@ access(all) fun testEmptyDeploymentUpdaterInitFails() { Test.expect(txResult, Test.beFailed()) } -access(all) fun testSetupMultiContractMultiAccountUpdaterSucceeds() { +access(all) fun testSetupMultiContractMultiAccountUpdater() { let contractAddresses: [Address] = [aAccount.address, bcAccount.address] let stage0: [{Address: {String: String}}] = [ { @@ -150,7 +111,6 @@ access(all) fun testSetupMultiContractMultiAccountUpdaterSucceeds() { [nil, contractAddresses, deploymentConfig], abcUpdater ) - Test.expect(setupUpdaterTxResult, Test.beSucceeded()) // Confirm UpdaterCreated event was properly emitted // TODO: Uncomment once bug is fixed allowing contract import @@ -362,6 +322,28 @@ access(all) fun testDelegationOfCompletedUpdaterFails() { Test.expect(txResult, Test.beFailed()) } +access(all) fun testCoordinatorSetBlockUpdateBoundaryFails() { + let txResult = executeTransaction( + "../transactions/coordinator/set_block_update_boundary.cdc", + [1], + admin + ) + Test.expect(txResult, Test.beFailed()) +} + +access(all) fun testCoordinatorSetBlockUpdateBoundarySucceeds() { + let txResult = executeTransaction( + "../transactions/coordinator/set_block_update_boundary.cdc", + [getCurrentBlockHeight() + blockHeightBoundaryDelay], + admin + ) + Test.expect(txResult, Test.beSucceeded()) + + // TODO: Uncomment once bug is fixed allowing contract import + // events = Test.eventsOfType(Type()) + // Test.assertEqual(1, events.length) +} + /* --- TEST HELPERS --- */ access(all) fun jumpToUpdateBoundary(forUpdater: Address) { diff --git a/transactions/coordinator/set_block_update_boundary.cdc b/transactions/coordinator/set_block_update_boundary.cdc index 5c16b0f..a16d653 100644 --- a/transactions/coordinator/set_block_update_boundary.cdc +++ b/transactions/coordinator/set_block_update_boundary.cdc @@ -3,20 +3,9 @@ import "StagedContractUpdates" /// Allows the contract Coordinator to set a new blockUpdateBoundary /// transaction(newBoundary: UInt64) { - - let coordinator: &StagedContractUpdates.Coordinator - prepare(signer: AuthAccount) { - self.coordinator = signer.borrow<&StagedContractUpdates.Coordinator>( - from: StagedContractUpdates.CoordinatorStoragePath - ) ?? panic("Could not borrow reference to Coordinator!") - } - - execute { - self.coordinator.setBlockUpdateBoundary(new: newBoundary) - } - - post { - StagedContractUpdates.blockUpdateBoundary == newBoundary: "Problem setting block update boundary" + signer.borrow<&StagedContractUpdates.Coordinator>(from: StagedContractUpdates.CoordinatorStoragePath) + ?.setBlockUpdateBoundary(new: newBoundary) + ?? panic("Could not borrow reference to Coordinator!") } } diff --git a/transactions/delegatee/set_block_update_boundary.cdc b/transactions/delegatee/set_block_update_boundary.cdc deleted file mode 100644 index e01d68a..0000000 --- a/transactions/delegatee/set_block_update_boundary.cdc +++ /dev/null @@ -1,19 +0,0 @@ -import "StagedContractUpdates" - -transaction(blockUpdateBoundary: UInt64) { - - let delegatee: &StagedContractUpdates.Delegatee - - prepare(signer: AuthAccount) { - self.delegatee = signer.borrow<&StagedContractUpdates.Delegatee>(from: StagedContractUpdates.DelegateeStoragePath) - ?? panic("Could not borrow a reference to the signer's Delegatee") - } - - execute { - self.delegatee.setBlockUpdateBoundary(blockHeight: blockUpdateBoundary) - } - - post { - self.delegatee.getBlockUpdateBoundary() == blockUpdateBoundary: "Problem setting block update boundary" - } -} diff --git a/transactions/updater/setup_updater_single_account_and_contract.cdc b/transactions/updater/setup_updater_single_account_and_contract.cdc index fb86e7e..eda6d07 100644 --- a/transactions/updater/setup_updater_single_account_and_contract.cdc +++ b/transactions/updater/setup_updater_single_account_and_contract.cdc @@ -41,14 +41,10 @@ transaction(blockHeightBoundary: UInt64?, contractName: String, code: String) { } let hostCap = signer.getCapability<&StagedContractUpdates.Host>(hostPrivatePath) - if blockHeightBoundary == nil && StagedContractUpdates.blockUpdateBoundary == nil { - // TODO: Refactor contract for generalized cases as can't setup Updater without a contract blockHeightBoundary - panic("Contract update boundary is not yet set, must specify blockHeightBoundary if not coordinating") - } // Create Updater resource, assigning the contract .blockUpdateBoundary to the new Updater signer.save( <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary!, + blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary, hosts: [hostCap], deployments: [[ StagedContractUpdates.ContractUpdate( From 561944b937645918ec120243e1627bfddf200819 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:26:42 -0600 Subject: [PATCH 16/40] revert StagedContractUpdates related transaction --- transactions/updater/setup_updater_multi_account.cdc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/transactions/updater/setup_updater_multi_account.cdc b/transactions/updater/setup_updater_multi_account.cdc index 0bbfb05..2f6ed62 100644 --- a/transactions/updater/setup_updater_multi_account.cdc +++ b/transactions/updater/setup_updater_multi_account.cdc @@ -37,13 +37,9 @@ transaction(blockHeightBoundary: UInt64?, contractAddresses: [Address], deployme // Construct deployment from config let deployments: [[StagedContractUpdates.ContractUpdate]] = StagedContractUpdates.getDeploymentFromConfig(deploymentConfig) - if blockHeightBoundary == nil && StagedContractUpdates.blockUpdateBoundary == nil { - // TODO: Refactor contract for generalized cases as can't setup Updater without a contract blockHeightBoundary - panic("Contract update boundary is not yet set, must specify blockHeightBoundary if not coordinating") - } // Construct the updater, save and link public Capability let contractUpdater: @StagedContractUpdates.Updater <- StagedContractUpdates.createNewUpdater( - blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary!, + blockUpdateBoundary: blockHeightBoundary ?? StagedContractUpdates.blockUpdateBoundary, hosts: hostCaps, deployments: deployments ) From 92bf71994468c924eb30a8612aa7255077b79a93 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:05:01 -0600 Subject: [PATCH 17/40] restructure repo for emphasis on Cadence 1.0 staging --- README.md | 285 ++++++------------ contracts/staged-contract-updates/README.md | 216 +++++++++++++ .../StagedContractUpdates.cdc | 0 flow.json | 2 +- setup.sh => staged_contract_updates_setup.sh | 0 tests/staged_contract_updater_tests.cdc | 2 +- 6 files changed, 317 insertions(+), 188 deletions(-) create mode 100644 contracts/staged-contract-updates/README.md rename contracts/{ => staged-contract-updates}/StagedContractUpdates.cdc (100%) rename setup.sh => staged_contract_updates_setup.sh (100%) diff --git a/README.md b/README.md index 22f3886..00c45c9 100644 --- a/README.md +++ b/README.md @@ -1,216 +1,129 @@ -# StagedContractUpdates +# Onchain Contract Update Mechanisms ![Tests](https://github.com/onflow/contract-updater/actions/workflows/ci.yml/badge.svg) [![codecov](https://codecov.io/gh/onflow/contract-updater/graph/badge.svg?token=TAIKIA95FU)](https://codecov.io/gh/onflow/contract-updater) -> Enables pre-defined contract update deployments to a set of wrapped account at or beyond a specified block height. For -> more details about the purpose of this mechanism, see [FLIP 179](https://github.com/onflow/flips/pull/179) +This repo contains contracts enabling onchain staging of contract updates, providing mechanisms to store code, +delegate update capabilities, and execute staged updates. -## Simple Case Demo +## Overview -For this run through, we'll focus on the simple case where a single contract is deployed to a single account that can -sign the setup & delegation transactions. +> :information_source: This document proceeds with an emphasis on the `MigrationContractStaging` contract, which will be +> used for the upcoming Cadence 1.0 network migration. Any contracts currently on Mainnet **WILL** need to be updated +> via state migration on the Cadence 1.0 milestone. This means you **MUST** stage your contract updates before the +> milestone for your contract to continue functioning. Keep reading to understand how to stage your contract update. -This use case is enough to get the basic concepts involved in the `StagedContractUpdates` contract, but know that more -advanced deployments are possible with support for multiple contract accounts and customized deployment configurations. +The `MigrationContractStaging` contract provides a mechanism for staging contract updates onchain in preparation for +Cadence 1.0. Once you have refactored your existing contracts to be Cadence 1.0 compatible, you will need to stage your +code in this contract for network state migrations to take effect and your contract to be updated with the Height +Coordinated Upgrade. -### Setup +### `MigrationContractStaging` Deployments -1. Start your local emulator: +> :information_source: The `MigrationContractStaging` contract is not yet deployed. Its deployment address will be added +> here once it has been deployed. - ```sh - flow emulator - ``` - -1. Setup emulator environment - this creates our emulator accounts & deploys contracts: - - ```sh - sh setup.sh - ``` - -### Walkthrough - -1. We can see that the `Foo` has been deployed, and call its only contract method `foo()`, getting back `"foo"`: - - ```sh - flow scripts execute ./scripts/test/foo.cdc - ``` - -1. Configure `StagedContractUpdates.Updater`, passing the block height, contract name, and contract code in hex form (see - [`get_code_hex.py`](./src/get_code_hex.py) for simple script hexifying contract code): - - `setup_updater_single_account_and_contract.cdc` - 1. `blockUpdateBoundary: UInt64` - 1. `contractName: String` - 1. `code: [String]` - - ```sh - flow transactions send ./transactions/updater/setup_updater_single_account_and_contract.cdc \ - 10 "Foo" 70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d \ - --signer foo - ``` - -1. Simulate block creation, running transactions to iterate over blocks to the pre-configured block update height: - - ```sh - sh tick_tock.sh - ``` - -1. We can get details from our `Updater` before updating: +| Network | Address | +|---|---| +| Testnet | TBD | +| Mainnet | TBD | - ```sh - flow scripts execute ./scripts/updater/get_updater_info.cdc 0xe03daebed8ca0615 - ``` - - ```sh - flow scripts execute ./scripts/updater/get_updater_deployment.cdc 0xe03daebed8ca0615 - ``` - -1. Next, we'll delegate the `Updater` Capability to the `Delegatee` stored in the `StagedContractUpdates`'s account. - - ```sh - flow transactions send ./transactions/delegate.cdc --signer foo - ``` - -1. Lastly, we'll run the updating transaction as the `Delegatee`: +### Pre-Requisites +- An existing contract deployed to your target network. For example, if you're staging `A` in address `0x01`, you should + already have a contract named `A` deployed to `0x01`. +- A Cadence 1.0 compatible contract serving as an update to your existing contract. Extending our example, if you're + staging `A` in address `0x01`, you should have a contract named `A` that is Cadence 1.0 compatible. See the references + below for more information on Cadence 1.0 language changes. +- Your contract as a hex string. + - Included in this repo is a Python util to hex-encode your contract which outputs your contract's code as a hex + string. With Python installed, run: ```sh - flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc + python3 ./src/get_code_hex.py ``` -1. And we can validate the update has taken place by calling `Foo.foo()` again and seeing the return value is now - `"bar"` +### Staging Your Contract Update - ```sh - flow scripts execute ./scripts/test/foo.cdc - ``` - -## Multi-Account Multi-Contract Deployment - -As mentioned above, `StagedContractUpdates` supports update deployments across any number of accounts & contracts. - -Developers with a number of owned contracts will find this helpful as they can specify the order in which an update -should occur according to the contract set's dependency graph. - -In our example, our dependency graph will look like this: - -![flat dependency dag](./resources/dependency_dag.png) - -So the contracts should be updated in the following order: +Armed with your pre-requisites, you're ready to stage your contract update. Simply run the [`stage_contract.cdc` +transaction](./transactions/migration-contract-staging/stage_contract.cdc), passing your contract's name and hex string +as arguments and signing as the contract host account. +```sh +flow transactions send ./transactions/migration-contract-staging/stage_contract.cdc \ + \ + --signer \ + --network ``` -[A, B, C] -``` - -This is because, assuming some breaking change prior to the update boundary, updating `C` before it's dependencies will -result in a failed deployment as contracts `A` & `B` are still in a broken state and cannot be imported when `C` is -updated. -However, since contract updates take effect **after** the updating transaction completes, we need to stage deployments -among updating transactions based on the depth of each contract in its dependency tree. - -More concretely, if we try to update all three contracts in the same transaction as above - `[A, B, C]` in sequence - -`B`'s dependency (`A`) will not have completed its update, causing `B`'s update attempt to fail. - -Consequently, we instead batch updates based on the contract's maximum depth in the dependency graph. In our case, -instead of `[A, B, C]` we update `A` in one transaction, `B` in the next, and lastly `C` can be updated. - -![dependency graph with node depth](./resources/dependency_dag_with_depth.png) - -This concept can be extrapolated out for larger dependency graphs. For example, take the following: - -![larger dag example](./resources/larger_dag.png) - -This group of contracts would be updated over the same three stages, with each stage including contracts according to -their maximum depth in the dependency graph. In this case: - -- Stage 0: `[A, D]` -- Stage 1: `[B, E]` -- Stage 2: `[C]` - -Let's continue into a walkthrough with contracts `A`, `B`, and `C` and see how `StagedContractUpdates` can be configured to -execute these pre-configured updates. - -### CLI Walkthrough - -For the following walkthrough, we'll assume `A` is deployed on its own account while `B` & `C` are in a different -account. - -:information_source: If you haven't already, perform the [setup steps above](#setup) - -1. Since we'll be configuring an update deployment across a number of contract accounts, we'll need to delegate access - to those accounts via AuthAccount Capabilities on each. Running the following transaction will link and encapsulate - an AuthAccount Capability in a `Host` within the signer's account and publish a Capability on it for the account - where our `Updater` will live. - - ```sh - flow transactions send ./transactions/host/publish_host_capability.cdc \ - 0xe03daebed8ca0615 \ - --signer a-account - ``` - - ```sh - flow transactions send ./transactions/host/publish_host_capability.cdc \ - 0xe03daebed8ca0615 \ - --signer bc-account - ``` - - :information_source: Note we perform a transaction for each account hosting contracts we will be updating. This - allows the `Updater` to perform updates for contracts across an arbitrary number of accounts. - -1. Next, we claim those published AuthAccount Capabilities and configure an `Updater` resource that contains them along - with our ordered deployment. - - `setup_updater_multi_account.cdc` - 1. `blockUpdateBoundary: UInt64` - 1. `contractAddresses: [Address]` - 1. `deploymentConfig: [[{Address: {String: String}}]]` - - ```sh - flow transactions send transactions/updater/setup_updater_multi_account.cdc \ - --args-json "$(cat args.json)" \ - --signer abc-updater - ``` - - :information_source: Arguments are passed in Cadence JSON format since the values exceptionally long. Take a look at - the transaction and arguments to more deeply understand what's being passed around. - -1. You'll see a number of events emitted, one of them being `UpdaterCreated` with your `Updater`'s UUID. This means the - resource was created, so let's query against the updater account to get its info. +This will execute the following transaction: + +```cadence +import "MigrationContractStaging" + +transaction(contractName: String, contractCode: String) { + let host: &MigrationContractStaging.Host + + prepare(signer: AuthAccount) { + // Configure Host resource if needed + let hostStoragePath: StoragePath = MigrationContractStaging.deriveHostStoragePath(hostAddress: signer.address) + if signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath) == nil { + signer.save(<-MigrationContractStaging.createHost(), to: hostStoragePath) + } + // Assign Host reference + self.host = signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath)! + } + + execute { + // Call staging contract, storing the contract code that will update during Cadence 1.0 migration + // If code is already staged for the given contract, it will be overwritten. + MigrationContractStaging.stageContract(host: self.host, name: contractName, code: contractCode) + } + + post { + MigrationContractStaging.isStaged(address: self.host.address(), name: contractName): + "Problem while staging update" + } +} +``` - ```sh - flow scripts execute ./scripts/updater/get_updater_info.cdc 0xe03daebed8ca0615 - ``` +At the end of this transaction, your contract will be staged in the `MigrationContractStaging` account. If you staged +this contract's code previously, it will be overwritted by the code you provided in this transaction. - ```sh - flow scripts execute ./scripts/updater/get_updater_deployment.cdc 0xe03daebed8ca0615 - ``` +> :warning: NOTE: Staging your contract successfully does not mean that your contract code is correct. Your testing and +> validation processes should include testing your contract code against the Cadence 1.0 interpreter to ensure your +> contract will function as expected. -1. Now we'll delegate a Capability on the `Updater` to the `Delegatee`: +### Checking Staging Status - ```sh - flow transactions send ./transactions/updater/delegate.cdc --signer abc-updater - ``` +You may want to validate that your contract has been staged correctly. To do so, you can run the +[`get_staged_contract_code.cdc` script](./scripts/migration-contract-staging/get_staged_contract_code.cdc), passing the +address & name of the contract you're requesting. This script can also help you get the staged code for your +dependencies if the project owner has staged their code. -1. In the previous transaction we should see that the `UpdaterDelegationChanged` event includes the `Updater` UUID - previously emitted in the creation event and that the `delegated` value is `true`. Now, we'll act as the `Delegatee` - and execute the update. +```sh +flow scripts execute ./scripts/migration-contract-staging/get_staged_contract_code.cdc \ + \ + --network +``` - ```sh - flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc - ``` +Which runs the script: - This transaction calls `Updater.update()`, executing the first staged deployment, and updating contract `A`. Note - that the emitted event contains the name and address of the updated contracts and that the `updateComplete` field is - still `false`. This is because there are still incomplete deployment stages. Let's run the transaction again, this - time updating `B`. +```cadence +import "MigrationContractStaging" - ```sh - flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc - ``` +/// Returns the code as it is staged or nil if it not currently staged. +/// +access(all) fun main(contractAddress: Address, contractName: String): String? { + return MigrationContractStaging.getStagedContractCode(address: contractAddress, name: contractName) +} +``` - Now we see `B` has been updated, but we still have one more stage to complete. Let's complete the staged update. +## References - ```sh - flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc - ``` +More tooling is slated to support Cadence 1.0 code changes and will be added as it arises. For any real-time help, be +sure to join the [Flow discord](https://discord.com/invite/J6fFnh2xx6) and ask away in the developer channels! - And finally, we see that `C` was updated and `updateComplete` is now `true`. \ No newline at end of file +- [Cadence 1.0 contract migration plan](https://forum.flow.com/t/update-on-cadence-1-0-upgrade-plan/5597) +- [Cadence 1.0 language update breakdown](https://forum.flow.com/t/update-on-cadence-1-0/5197) +- [Cadence Language reference](https://cadence-lang.org/) +- [Emerald City's Cadence 1.0 by Example](https://academy.ecdao.org/en/cadence-by-example) \ No newline at end of file diff --git a/contracts/staged-contract-updates/README.md b/contracts/staged-contract-updates/README.md new file mode 100644 index 0000000..034ac98 --- /dev/null +++ b/contracts/staged-contract-updates/README.md @@ -0,0 +1,216 @@ +# StagedContractUpdates + +> :warning: This contract is general purpose and is not supporting Cadence 1.0 contract staging! Please refer to the +> `MigrationContractStaging` for reference on staging your contract for the network-wide coordinated update. + +Enables pre-defined contract update deployments to a set of wrapped account at or beyond a specified block height. For +more details about the purpose of this mechanism, see [FLIP 179](https://github.com/onflow/flips/pull/179) + +## Simple Case Demo + +For this run through, we'll focus on the simple case where a single contract is deployed to a single account that can +sign the setup & delegation transactions. + +This use case is enough to get the basic concepts involved in the `StagedContractUpdates` contract, but know that more +advanced deployments are possible with support for multiple contract accounts and customized deployment configurations. + +### Setup + +1. Start your local emulator: + + ```sh + flow emulator + ``` + +1. Setup emulator environment - this creates our emulator accounts & deploys contracts: + + ```sh + sh staged_contract_updates_setup.sh + ``` + +### Walkthrough + +1. We can see that the `Foo` has been deployed, and call its only contract method `foo()`, getting back `"foo"`: + + ```sh + flow scripts execute ./scripts/test/foo.cdc + ``` + +1. Configure `StagedContractUpdates.Updater`, passing the block height, contract name, and contract code in hex form (see + [`get_code_hex.py`](./src/get_code_hex.py) for simple script hexifying contract code): + - `setup_updater_single_account_and_contract.cdc` + 1. `blockUpdateBoundary: UInt64` + 1. `contractName: String` + 1. `code: [String]` + + ```sh + flow transactions send ./transactions/updater/setup_updater_single_account_and_contract.cdc \ + 10 "Foo" 70756220636f6e747261637420466f6f207b0a202020207075622066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d \ + --signer foo + ``` + +1. Simulate block creation, running transactions to iterate over blocks to the pre-configured block update height: + + ```sh + sh tick_tock.sh + ``` + +1. We can get details from our `Updater` before updating: + + ```sh + flow scripts execute ./scripts/updater/get_updater_info.cdc 0xe03daebed8ca0615 + ``` + + ```sh + flow scripts execute ./scripts/updater/get_updater_deployment.cdc 0xe03daebed8ca0615 + ``` + +1. Next, we'll delegate the `Updater` Capability to the `Delegatee` stored in the `StagedContractUpdates`'s account. + + ```sh + flow transactions send ./transactions/delegate.cdc --signer foo + ``` + +1. Lastly, we'll run the updating transaction as the `Delegatee`: + + ```sh + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc + ``` + +1. And we can validate the update has taken place by calling `Foo.foo()` again and seeing the return value is now + `"bar"` + + ```sh + flow scripts execute ./scripts/test/foo.cdc + ``` + +## Multi-Account Multi-Contract Deployment + +As mentioned above, `StagedContractUpdates` supports update deployments across any number of accounts & contracts. + +Developers with a number of owned contracts will find this helpful as they can specify the order in which an update +should occur according to the contract set's dependency graph. + +In our example, our dependency graph will look like this: + +![flat dependency dag](./resources/dependency_dag.png) + +So the contracts should be updated in the following order: + +``` +[A, B, C] +``` + +This is because, assuming some breaking change prior to the update boundary, updating `C` before it's dependencies will +result in a failed deployment as contracts `A` & `B` are still in a broken state and cannot be imported when `C` is +updated. + +However, since contract updates take effect **after** the updating transaction completes, we need to stage deployments +among updating transactions based on the depth of each contract in its dependency tree. + +More concretely, if we try to update all three contracts in the same transaction as above - `[A, B, C]` in sequence - +`B`'s dependency (`A`) will not have completed its update, causing `B`'s update attempt to fail. + +Consequently, we instead batch updates based on the contract's maximum depth in the dependency graph. In our case, +instead of `[A, B, C]` we update `A` in one transaction, `B` in the next, and lastly `C` can be updated. + +![dependency graph with node depth](./resources/dependency_dag_with_depth.png) + +This concept can be extrapolated out for larger dependency graphs. For example, take the following: + +![larger dag example](./resources/larger_dag.png) + +This group of contracts would be updated over the same three stages, with each stage including contracts according to +their maximum depth in the dependency graph. In this case: + +- Stage 0: `[A, D]` +- Stage 1: `[B, E]` +- Stage 2: `[C]` + +Let's continue into a walkthrough with contracts `A`, `B`, and `C` and see how `StagedContractUpdates` can be configured to +execute these pre-configured updates. + +### CLI Walkthrough + +For the following walkthrough, we'll assume `A` is deployed on its own account while `B` & `C` are in a different +account. + +:information_source: If you haven't already, perform the [setup steps above](#setup) + +1. Since we'll be configuring an update deployment across a number of contract accounts, we'll need to delegate access + to those accounts via AuthAccount Capabilities on each. Running the following transaction will link and encapsulate + an AuthAccount Capability in a `Host` within the signer's account and publish a Capability on it for the account + where our `Updater` will live. + + ```sh + flow transactions send ./transactions/host/publish_host_capability.cdc \ + 0xe03daebed8ca0615 \ + --signer a-account + ``` + + ```sh + flow transactions send ./transactions/host/publish_host_capability.cdc \ + 0xe03daebed8ca0615 \ + --signer bc-account + ``` + + :information_source: Note we perform a transaction for each account hosting contracts we will be updating. This + allows the `Updater` to perform updates for contracts across an arbitrary number of accounts. + +1. Next, we claim those published AuthAccount Capabilities and configure an `Updater` resource that contains them along + with our ordered deployment. + - `setup_updater_multi_account.cdc` + 1. `blockUpdateBoundary: UInt64` + 1. `contractAddresses: [Address]` + 1. `deploymentConfig: [[{Address: {String: String}}]]` + + ```sh + flow transactions send transactions/updater/setup_updater_multi_account.cdc \ + --args-json "$(cat args.json)" \ + --signer abc-updater + ``` + + :information_source: Arguments are passed in Cadence JSON format since the values exceptionally long. Take a look at + the transaction and arguments to more deeply understand what's being passed around. + +1. You'll see a number of events emitted, one of them being `UpdaterCreated` with your `Updater`'s UUID. This means the + resource was created, so let's query against the updater account to get its info. + + ```sh + flow scripts execute ./scripts/updater/get_updater_info.cdc 0xe03daebed8ca0615 + ``` + + ```sh + flow scripts execute ./scripts/updater/get_updater_deployment.cdc 0xe03daebed8ca0615 + ``` + +1. Now we'll delegate a Capability on the `Updater` to the `Delegatee`: + + ```sh + flow transactions send ./transactions/updater/delegate.cdc --signer abc-updater + ``` + +1. In the previous transaction we should see that the `UpdaterDelegationChanged` event includes the `Updater` UUID + previously emitted in the creation event and that the `delegated` value is `true`. Now, we'll act as the `Delegatee` + and execute the update. + + ```sh + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc + ``` + + This transaction calls `Updater.update()`, executing the first staged deployment, and updating contract `A`. Note + that the emitted event contains the name and address of the updated contracts and that the `updateComplete` field is + still `false`. This is because there are still incomplete deployment stages. Let's run the transaction again, this + time updating `B`. + + ```sh + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc + ``` + + Now we see `B` has been updated, but we still have one more stage to complete. Let's complete the staged update. + + ```sh + flow transactions send ./transactions/delegatee/execute_all_delegated_updates.cdc + ``` + + And finally, we see that `C` was updated and `updateComplete` is now `true`. \ No newline at end of file diff --git a/contracts/StagedContractUpdates.cdc b/contracts/staged-contract-updates/StagedContractUpdates.cdc similarity index 100% rename from contracts/StagedContractUpdates.cdc rename to contracts/staged-contract-updates/StagedContractUpdates.cdc diff --git a/flow.json b/flow.json index c7106fa..b14da8d 100644 --- a/flow.json +++ b/flow.json @@ -59,7 +59,7 @@ } }, "StagedContractUpdates": { - "source": "./contracts/StagedContractUpdates.cdc", + "source": "./contracts/staged-contract-updates/StagedContractUpdates.cdc", "aliases": { "emulator": "f8d6e0586b0a20c7", "testing": "0000000000000007" diff --git a/setup.sh b/staged_contract_updates_setup.sh similarity index 100% rename from setup.sh rename to staged_contract_updates_setup.sh diff --git a/tests/staged_contract_updater_tests.cdc b/tests/staged_contract_updater_tests.cdc index 3e68b99..8dc2040 100644 --- a/tests/staged_contract_updater_tests.cdc +++ b/tests/staged_contract_updater_tests.cdc @@ -22,7 +22,7 @@ access(all) let cUpdateCode = "696d706f727420412066726f6d20307830303030303030303 access(all) fun setup() { var err = Test.deployContract( name: "StagedContractUpdates", - path: "../contracts/StagedContractUpdates.cdc", + path: "../contracts/staged-contract-updates/StagedContractUpdates.cdc", arguments: [getCurrentBlockHeight() + blockHeightBoundaryDelay] ) Test.expect(err, Test.beNil()) From c17eaec186b61a243295dbfcff3b64f246294aa7 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:11:46 -0600 Subject: [PATCH 18/40] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 00c45c9..0bbe258 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,9 @@ this contract's code previously, it will be overwritted by the code you provided ### Checking Staging Status -You may want to validate that your contract has been staged correctly. To do so, you can run the -[`get_staged_contract_code.cdc` script](./scripts/migration-contract-staging/get_staged_contract_code.cdc), passing the -address & name of the contract you're requesting. This script can also help you get the staged code for your +You may later want to retrieve your contract's staged code. To do so, you can run the [`get_staged_contract_code.cdc` +script](./scripts/migration-contract-staging/get_staged_contract_code.cdc), passing the address & name of the contract +you're requesting and getting the Cadence code in return. This script can also help you get the staged code for your dependencies if the project owner has staged their code. ```sh From 64fb14036efb39237c0eba0bce193804b2e7a509 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:17:20 -0600 Subject: [PATCH 19/40] add contract comments --- contracts/MigrationContractStaging.cdc | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 131bedf..a8cc4e6 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -49,7 +49,8 @@ access(all) contract MigrationContractStaging { /// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage /// /// Stages the contract code for the given contract name at the host address. If the contract is already staged, - /// the code will be replaced. + /// the code will be replaced. + /// access(all) fun stageContract(host: &Host, name: String, code: String) { if self.stagedContracts[host.address()] == nil { self.stagedContracts.insert(key: host.address(), [name]) @@ -66,7 +67,7 @@ access(all) contract MigrationContractStaging { } } - /// Removes the staged contract code from the staging environment + /// Removes the staged contract code from the staging environment. /// access(all) fun unstageContract(host: &Host, name: String) { pre { @@ -89,19 +90,19 @@ access(all) contract MigrationContractStaging { /* --- Public Getters --- */ - /// Returns true if the contract is currently staged + /// Returns true if the contract is currently staged. /// access(all) fun isStaged(address: Address, name: String): Bool { return self.stagedContracts[address]?.contains(name) ?? false } - /// Returns the names of all staged contracts for the given address + /// Returns the names of all staged contracts for the given address. /// access(all) fun getStagedContractNames(forAddress: Address): [String] { return self.stagedContracts[forAddress] ?? [] } - /// Returns the staged contract Cadence code for the given address and name + /// Returns the staged contract Cadence code for the given address and name. /// access(all) fun getStagedContractCode(address: Address, name: String): String? { let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) @@ -112,13 +113,13 @@ access(all) contract MigrationContractStaging { } } - /// Returns an array of all staged contract host addresses + /// Returns an array of all staged contract host addresses. /// access(all) view fun getAllStagedContractHosts(): [Address] { return self.stagedContracts.keys } - /// Returns a dictionary of all staged contract code for the given address + /// Returns a dictionary of all staged contract code for the given address. /// access(all) fun getAllStagedContractCode(forAddress: Address): {String: String} { if self.stagedContracts[forAddress] == nil { @@ -169,7 +170,7 @@ access(all) contract MigrationContractStaging { ContractUpdate ********************/ - /// Represents contract and its corresponding code + /// Represents contract and its corresponding code. /// access(all) struct ContractUpdate { access(all) let address: Address @@ -182,7 +183,7 @@ access(all) contract MigrationContractStaging { self.code = code } - /// Validates that the named contract exists at the target address + /// Validates that the named contract exists at the target address. /// access(all) view fun isValid(): Bool { return getAccount(self.address).contracts.names.contains(self.name) @@ -194,12 +195,14 @@ access(all) contract MigrationContractStaging { return self.address.toString().concat(".").concat(self.name) } - /// Returns human-readable string of the Cadence code + /// Returns human-readable string of the Cadence code. /// access(all) view fun codeAsCadence(): String { return String.fromUTF8(self.code.decodeHex()) ?? panic("Problem stringifying code!") } + /// Replaces the ContractUpdate code with that provided. + /// access(contract) fun replaceCode(_ code: String) { self.code = code } @@ -231,7 +234,7 @@ access(all) contract MigrationContractStaging { /// Cadence 1.0 milestone. /// access(all) resource Capsule { - /// The address, name and code of the contract that will be updated + /// The address, name and code of the contract that will be updated. access(self) let update: ContractUpdate init(update: ContractUpdate) { @@ -242,13 +245,13 @@ access(all) contract MigrationContractStaging { self.update = update } - /// Returns the staged contract update in the form of a ContractUpdate struct + /// Returns the staged contract update in the form of a ContractUpdate struct. /// access(all) view fun getContractUpdate(): ContractUpdate { return self.update } - /// Replaces the staged contract code with the given hex-encoded Cadence code + /// Replaces the staged contract code with the given hex-encoded Cadence code. /// access(contract) fun replaceCode(code: String) { post { @@ -285,6 +288,9 @@ access(all) contract MigrationContractStaging { return <- capsule } + /// Removes the staged update's Capsule from storage and returns the UUID of the removed Capsule or nil if it + /// wasn't found. Also removes the contract name from the stagedContracts mapping. + /// access(self) fun removeStagedContract(address: Address, name: String): UInt64? { let contractIndex: Int = self.stagedContracts[address]!.firstIndex(of: name)! self.stagedContracts[address]!.remove(at: contractIndex) @@ -295,6 +301,9 @@ access(all) contract MigrationContractStaging { return self.destroyCapsule(address: address, name: name) } + /// Destroys the Capsule resource at the derived path in this contract's account storage and returns the UUID of + /// the destroyed Capsule if it existed. + /// access(self) fun destroyCapsule(address: Address, name: String): UInt64? { let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) if let capsule <- self.account.load<@Capsule>(from: capsulePath) { From e4657ea20ddbe482ab0932aec78b413b2859f0b0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:43:39 -0600 Subject: [PATCH 20/40] add initial MigrationContractStaging tests --- flow.json | 3 +- .../get_all_staged_contract_hosts.cdc | 2 +- tests/migration_contract_staging_tests.cdc | 112 ++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/migration_contract_staging_tests.cdc diff --git a/flow.json b/flow.json index b14da8d..b086e0a 100644 --- a/flow.json +++ b/flow.json @@ -47,7 +47,8 @@ "MigrationContractStaging": { "source": "./contracts/MigrationContractStaging.cdc", "aliases": { - "emulator": "f8d6e0586b0a20c7" + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" } }, "NonFungibleToken": { diff --git a/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc b/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc index 17949e4..8cd3b9b 100644 --- a/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc +++ b/scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc @@ -2,6 +2,6 @@ import "MigrationContractStaging" /// Returns the code for all staged contracts hosted by the given contract address. /// -access(all) fun main(contractAddress: Address): [Address] { +access(all) fun main(): [Address] { return MigrationContractStaging.getAllStagedContractHosts() } diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc new file mode 100644 index 0000000..8c8b8d0 --- /dev/null +++ b/tests/migration_contract_staging_tests.cdc @@ -0,0 +1,112 @@ +import Test +import BlockchainHelpers + +// Contract hosts as defined in flow.json +access(all) let fooAccount = Test.getAccount(0x0000000000000008) +access(all) let aAccount = Test.getAccount(0x0000000000000009) +access(all) let bcAccount = Test.getAccount(0x0000000000000010) + +// Content of update contracts as hex strings +access(all) let fooUpdateCode = "61636365737328616c6c2920636f6e747261637420466f6f207b0a2020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" +access(all) let aUpdateCode = "61636365737328616c6c2920636f6e747261637420696e746572666163652041207b0a202020200a2020202061636365737328616c6c29207265736f7572636520696e746572666163652049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e670a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f757263652052203a2049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a7d" +access(all) let bUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a0a61636365737328616c6c2920636f6e74726163742042203a2041207b0a202020200a2020202061636365737328616c6c29207265736f757263652052203a20412e49207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a2020202061636365737328616c6c292066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" +access(all) let cUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a696d706f727420422066726f6d203078303030303030303030303030303031300a0a61636365737328616c6c2920636f6e74726163742043207b0a0a2020202061636365737328616c6c29206c65742053746f72616765506174683a2053746f72616765506174680a2020202061636365737328616c6c29206c6574205075626c6963506174683a205075626c6963506174680a0a2020202061636365737328616c6c29207265736f7572636520696e74657266616365204f757465725075626c6963207b0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f75726365204f75746572203a204f757465725075626c6963207b0a202020202020202061636365737328616c6c29206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" + +access(all) fun setup() { + var err = Test.deployContract( + name: "MigrationContractStaging", + path: "../contracts/MigrationContractStaging.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "Foo", + path: "../contracts/test/Foo.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "A", + path: "../contracts/test/A.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "B", + path: "../contracts/test/B.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "C", + path: "../contracts/test/C.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +access(all) fun testStagedNonExistentContractFails() { + let alice = Test.createAccount() + let txResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["A", aUpdateCode], + alice + ) + Test.expect(txResult, Test.beFailed()) +} + +access(all) fun testStagedContractSucceeds() { + let txResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["Foo", fooUpdateCode], + fooAccount + ) + Test.expect(txResult, Test.beSucceeded()) + + let isStagedResult = executeScript( + "../scripts/migration-contract-staging/is_staged.cdc", + [fooAccount.address, "Foo"] + ) + let isStaged = isStagedResult.returnValue as! Bool? ?? panic("Problem retrieving result of isStaged()") + Test.assertEqual(true, isStaged) + + let stagedContractNamesResult = executeScript( + "../scripts/migration-contract-staging/get_staged_contract_names_for_address.cdc", + [fooAccount.address] + ) + let fooAccountStagedContractNames = stagedContractNamesResult.returnValue as! [String]? + ?? panic("Problem retrieving result of getAllStagedContractNamesForAddress()") + Test.assert(fooAccountStagedContractNames.length == 1, message: "Invalid number of staged contracts on fooAccount") + + let allStagedContractHostsResult = executeScript( + "../scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc", + [] + ) + let allStagedContractHosts = allStagedContractHostsResult.returnValue as! [Address]? + ?? panic("Problem retrieving result of getAllStagedContractHosts()") + Test.assertEqual([fooAccount.address], allStagedContractHosts) + + let stagedContractCodeResult = executeScript( + "../scripts/migration-contract-staging/get_staged_contract_code.cdc", + [fooAccount.address, "Foo"] + ) + let fooStagedContractCode = stagedContractCodeResult.returnValue as! String? + ?? panic("Problem retrieving result of getStagedContractCode()") + Test.assertEqual(fooUpdateCode, String.encodeHex(fooStagedContractCode.utf8)) + + let allStagedContractCodeForAddressResult = executeScript( + "../scripts/migration-contract-staging/get_all_staged_contract_code_for_address.cdc", + [fooAccount.address] + ) + let allStagedCodeForFoo = allStagedContractCodeForAddressResult.returnValue as! {String: String}? + ?? panic("Problem retrieving result of getAllStagedContractCodeForAddress()") + Test.assert(allStagedCodeForFoo.length == 1, message: "Invalid number of staged contracts for fooAccount") + Test.assertEqual( + { "Foo": fooUpdateCode }, + { allStagedCodeForFoo.keys[0]: String.encodeHex(allStagedCodeForFoo["Foo"]!.utf8) } + ) +} From 47b03adb7470c15fc4902d7cef9b4e8756331b0e Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:01:01 -0600 Subject: [PATCH 21/40] fix MigrationContractStaging.stageContract bug --- contracts/MigrationContractStaging.cdc | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index a8cc4e6..7e79c19 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -52,18 +52,25 @@ access(all) contract MigrationContractStaging { /// the code will be replaced. /// access(all) fun stageContract(host: &Host, name: String, code: String) { + let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) if self.stagedContracts[host.address()] == nil { + // First time we're seeing contracts from this address - insert the address and contract name self.stagedContracts.insert(key: host.address(), [name]) + // Create a new Capsule to store the staged code let capsule: @Capsule <- self.createCapsule(host: host, name: name, code: code) - - self.account.save(<-capsule, to: self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name)) + self.account.save(<-capsule, to: capsulePath) } else { + // We've seen contracts from this host address before if let contractIndex: Int = self.stagedContracts[host.address()]!.firstIndex(of: name) { - self.stagedContracts[host.address()]!.remove(at: contractIndex) + // The contract is already staged - replace the code + let capsule: &Capsule = self.account.borrow<&Capsule>(from: capsulePath) + ?? panic("Could not borrow existing Capsule from storage for staged contract") + capsule.replaceCode(code: code) + } else { + // First time staging this contrac - add the contract name to the list of contracts staged for host + self.stagedContracts[host.address()]!.append(name) + self.account.save(<-self.createCapsule(host: host, name: name, code: code), to: capsulePath) } - self.stagedContracts[host.address()]!.append(name) - let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) - self.account.borrow<&Capsule>(from: capsulePath)!.replaceCode(code: code) } } From ddbed96e005ea829e2cf392c77edf79f59dc100a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:01:16 -0600 Subject: [PATCH 22/40] add MigrationContractStaging test coverage --- normalize_coverage_report.sh | 3 +- tests/migration_contract_staging_tests.cdc | 153 ++++++++++++++++++--- 2 files changed, 135 insertions(+), 21 deletions(-) diff --git a/normalize_coverage_report.sh b/normalize_coverage_report.sh index 6dcbee8..3dde94d 100755 --- a/normalize_coverage_report.sh +++ b/normalize_coverage_report.sh @@ -1,4 +1,5 @@ -sed -i 's/A.0000000000000007.StagedContractUpdates/contracts\/StagedContractUpdates.cdc/' coverage.lcov +sed -i 's/A.0000000000000007.StagedContractUpdates/contracts\/staged-contract-updates\/StagedContractUpdates.cdc/' coverage.lcov +sed -i 's/A.0000000000000007.MigrationContractStaging/contracts\/MigrationContractStaging.cdc/' coverage.lcov sed -i 's/A.0000000000000008.Foo/contracts\/example\/Foo.cdc/' coverage.lcov sed -i 's/A.0000000000000009.A/contracts\/example\/A.cdc/' coverage.lcov sed -i 's/A.0000000000000010.B/contracts\/example\/B.cdc/' coverage.lcov diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc index 8c8b8d0..bda9a53 100644 --- a/tests/migration_contract_staging_tests.cdc +++ b/tests/migration_contract_staging_tests.cdc @@ -8,9 +8,13 @@ access(all) let bcAccount = Test.getAccount(0x0000000000000010) // Content of update contracts as hex strings access(all) let fooUpdateCode = "61636365737328616c6c2920636f6e747261637420466f6f207b0a2020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a202020202020202072657475726e2022626172220a202020207d0a7d0a" +access(all) let fooUpdateCadence = String.fromUTF8(fooUpdateCode.decodeHex()) ?? panic("Problem decoding fooUpdateCode") access(all) let aUpdateCode = "61636365737328616c6c2920636f6e747261637420696e746572666163652041207b0a202020200a2020202061636365737328616c6c29207265736f7572636520696e746572666163652049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e670a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f757263652052203a2049207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a7d" +access(all) let aUpdateCadence = String.fromUTF8(aUpdateCode.decodeHex()) ?? panic("Problem decoding aUpdateCode") access(all) let bUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a0a61636365737328616c6c2920636f6e74726163742042203a2041207b0a202020200a2020202061636365737328616c6c29207265736f757263652052203a20412e49207b0a202020202020202061636365737328616c6c292066756e20666f6f28293a20537472696e67207b0a20202020202020202020202072657475726e2022666f6f220a20202020202020207d0a202020202020202061636365737328616c6c292066756e2062617228293a20537472696e67207b0a20202020202020202020202072657475726e2022626172220a20202020202020207d0a202020207d0a202020200a2020202061636365737328616c6c292066756e206372656174655228293a204052207b0a202020202020202072657475726e203c2d637265617465205228290a202020207d0a7d" +access(all) let bUpdateCadence = String.fromUTF8(bUpdateCode.decodeHex()) ?? panic("Problem decoding bUpdateCode") access(all) let cUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a696d706f727420422066726f6d203078303030303030303030303030303031300a0a61636365737328616c6c2920636f6e74726163742043207b0a0a2020202061636365737328616c6c29206c65742053746f72616765506174683a2053746f72616765506174680a2020202061636365737328616c6c29206c6574205075626c6963506174683a205075626c6963506174680a0a2020202061636365737328616c6c29207265736f7572636520696e74657266616365204f757465725075626c6963207b0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f75726365204f75746572203a204f757465725075626c6963207b0a202020202020202061636365737328616c6c29206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" +access(all) let cUpdateCadence = String.fromUTF8(cUpdateCode.decodeHex()) ?? panic("Problem decoding cUpdateCode") access(all) fun setup() { var err = Test.deployContract( @@ -57,9 +61,10 @@ access(all) fun testStagedNonExistentContractFails() { alice ) Test.expect(txResult, Test.beFailed()) + assertIsStaged(contractAddress: alice.address, contractName: "A", invert: true) } -access(all) fun testStagedContractSucceeds() { +access(all) fun testStageContractSucceeds() { let txResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", ["Foo", fooUpdateCode], @@ -67,46 +72,154 @@ access(all) fun testStagedContractSucceeds() { ) Test.expect(txResult, Test.beSucceeded()) + assertIsStaged(contractAddress: fooAccount.address, contractName: "Foo", invert: false) + + let fooAccountStagedContractNames = getStagedContractNamesForAddress(fooAccount.address) + Test.assert(fooAccountStagedContractNames.length == 1, message: "Invalid number of staged contracts on fooAccount") + + let allStagedContractHosts = getAllStagedContractHosts() + assertAddressArraysEqual([fooAccount.address], allStagedContractHosts) + + let fooStagedContractCode = getStagedContractCode(contractAddress: fooAccount.address, contractName: "Foo") + ?? panic("Problem retrieving result of getStagedContractCode()") + Test.assertEqual(fooUpdateCode, String.encodeHex(fooStagedContractCode.utf8)) + + let allStagedCodeForFooAccount = getAllStagedContractCodeForAddress(contractAddress: fooAccount.address) + assertStagedContractCodeEqual({ "Foo": fooUpdateCadence}, allStagedCodeForFooAccount) +} + +access(all) fun testStageMultipleContractsSucceeds() { + // Demonstrating staging multiple contracts on the same host & out of dependency order + let cStagingTxResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["C", cUpdateCode], + bcAccount + ) + Test.expect(cStagingTxResult, Test.beSucceeded()) + let bStagingTxResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["B", bUpdateCode], + bcAccount + ) + Test.expect(bStagingTxResult, Test.beSucceeded()) + let aStagingTxResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["A", aUpdateCode], + aAccount + ) + Test.expect(aStagingTxResult, Test.beSucceeded()) + + assertIsStaged(contractAddress: aAccount.address, contractName: "A", invert: false) + assertIsStaged(contractAddress: bcAccount.address, contractName: "B", invert: false) + assertIsStaged(contractAddress: bcAccount.address, contractName: "C", invert: false) + + let aAccountStagedContractNames = getStagedContractNamesForAddress(aAccount.address) + let bcAccountStagedContractNames = getStagedContractNamesForAddress(bcAccount.address) + Test.assert(aAccountStagedContractNames.length == 1, message: "Invalid number of staged contracts on aAccount") + Test.assert(bcAccountStagedContractNames.length == 2, message: "Invalid number of staged contracts on bcAccount") + + let allStagedContractHosts = getAllStagedContractHosts() + assertAddressArraysEqual([fooAccount.address, aAccount.address, bcAccount.address], allStagedContractHosts) + + let aStagedCode = getStagedContractCode(contractAddress: aAccount.address, contractName: "A") + ?? panic("Problem retrieving result of getStagedContractCode()") + let bStagedCode = getStagedContractCode(contractAddress: bcAccount.address, contractName: "B") + ?? panic("Problem retrieving result of getStagedContractCode()") + let cStagedCode = getStagedContractCode(contractAddress: bcAccount.address, contractName: "C") + ?? panic("Problem retrieving result of getStagedContractCode()") + Test.assertEqual(aUpdateCode, String.encodeHex(aStagedCode.utf8)) + Test.assertEqual(bUpdateCode, String.encodeHex(bStagedCode.utf8)) + Test.assertEqual(cUpdateCode, String.encodeHex(cStagedCode.utf8)) + + let allStagedCodeForAAccount = getAllStagedContractCodeForAddress(contractAddress: aAccount.address) + let allStagedCodeForBCAccount = getAllStagedContractCodeForAddress(contractAddress: bcAccount.address) + assertStagedContractCodeEqual({ "A": aUpdateCadence }, allStagedCodeForAAccount) + assertStagedContractCodeEqual({ "B": bUpdateCadence, "C": cUpdateCadence }, allStagedCodeForBCAccount) +} + +access(all) fun testUnstageContractSucceeds() { + let txResult = executeTransaction( + "../transactions/migration-contract-staging/unstage_contract.cdc", + ["Foo"], + fooAccount + ) + Test.expect(txResult, Test.beSucceeded()) + + assertIsStaged(contractAddress: fooAccount.address, contractName: "Foo", invert: true) + + let fooAccountStagedContractNames = getStagedContractNamesForAddress(fooAccount.address) + Test.assert(fooAccountStagedContractNames.length == 0, message: "Invalid number of staged contracts on fooAccount") + + let allStagedContractHosts = getAllStagedContractHosts() + assertAddressArraysEqual([aAccount.address, bcAccount.address], allStagedContractHosts) + + let fooStagedContractCode = getStagedContractCode(contractAddress: fooAccount.address, contractName: "Foo") + Test.assertEqual(nil, fooStagedContractCode) + + let allStagedCodeForFooAccount = getAllStagedContractCodeForAddress(contractAddress: fooAccount.address) + assertStagedContractCodeEqual({}, allStagedCodeForFooAccount) +} + +/* --- Test Helpers --- */ + +access(all) fun assertIsStaged(contractAddress: Address, contractName: String, invert: Bool) { let isStagedResult = executeScript( "../scripts/migration-contract-staging/is_staged.cdc", - [fooAccount.address, "Foo"] + [contractAddress, contractName] ) let isStaged = isStagedResult.returnValue as! Bool? ?? panic("Problem retrieving result of isStaged()") - Test.assertEqual(true, isStaged) + Test.assertEqual(!invert, isStaged) +} +access(all) fun assertAddressArraysEqual(_ expected: [Address], _ actual: [Address]) { + Test.assert(expected.length == actual.length, message: "Arrays are of unequal length") + for address in expected { + Test.assert(actual.contains(address), message: "Actual array does not contain ".concat(address.toString())) + } +} + +access(all) fun assertStagedContractCodeEqual(_ expected: {String: String}, _ actual: {String: String}) { + Test.assert(expected.length == actual.length, message: "Staged code mappings are of unequal length") + expected.forEachKey(fun(contractName: String): Bool { + Test.assert( + actual[contractName] == expected[contractName], + message: "Mismatched code for contract ".concat(contractName) + ) + return true + }) +} + +access(all) fun getStagedContractNamesForAddress(_ address: Address): [String] { let stagedContractNamesResult = executeScript( "../scripts/migration-contract-staging/get_staged_contract_names_for_address.cdc", - [fooAccount.address] + [address] ) - let fooAccountStagedContractNames = stagedContractNamesResult.returnValue as! [String]? + return stagedContractNamesResult.returnValue as! [String]? ?? panic("Problem retrieving result of getAllStagedContractNamesForAddress()") - Test.assert(fooAccountStagedContractNames.length == 1, message: "Invalid number of staged contracts on fooAccount") +} +access(all) fun getAllStagedContractHosts(): [Address] { let allStagedContractHostsResult = executeScript( "../scripts/migration-contract-staging/get_all_staged_contract_hosts.cdc", [] ) - let allStagedContractHosts = allStagedContractHostsResult.returnValue as! [Address]? + return allStagedContractHostsResult.returnValue as! [Address]? ?? panic("Problem retrieving result of getAllStagedContractHosts()") - Test.assertEqual([fooAccount.address], allStagedContractHosts) +} +access(all) fun getStagedContractCode(contractAddress: Address, contractName: String): String? { let stagedContractCodeResult = executeScript( "../scripts/migration-contract-staging/get_staged_contract_code.cdc", - [fooAccount.address, "Foo"] + [contractAddress, contractName] ) - let fooStagedContractCode = stagedContractCodeResult.returnValue as! String? - ?? panic("Problem retrieving result of getStagedContractCode()") - Test.assertEqual(fooUpdateCode, String.encodeHex(fooStagedContractCode.utf8)) + return stagedContractCodeResult.returnValue as! String? +} +access(all) fun getAllStagedContractCodeForAddress(contractAddress: Address): {String: String} { let allStagedContractCodeForAddressResult = executeScript( "../scripts/migration-contract-staging/get_all_staged_contract_code_for_address.cdc", - [fooAccount.address] + [contractAddress] ) - let allStagedCodeForFoo = allStagedContractCodeForAddressResult.returnValue as! {String: String}? + return allStagedContractCodeForAddressResult.returnValue as! {String: String}? ?? panic("Problem retrieving result of getAllStagedContractCodeForAddress()") - Test.assert(allStagedCodeForFoo.length == 1, message: "Invalid number of staged contracts for fooAccount") - Test.assertEqual( - { "Foo": fooUpdateCode }, - { allStagedCodeForFoo.keys[0]: String.encodeHex(allStagedCodeForFoo["Foo"]!.utf8) } - ) -} +} \ No newline at end of file From d5de4de28f2217aaf4e6b3516071f1dd3391a947 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:05:51 -0600 Subject: [PATCH 23/40] add MigrationContractStaging test cases --- tests/migration_contract_staging_tests.cdc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc index bda9a53..42bf804 100644 --- a/tests/migration_contract_staging_tests.cdc +++ b/tests/migration_contract_staging_tests.cdc @@ -64,6 +64,17 @@ access(all) fun testStagedNonExistentContractFails() { assertIsStaged(contractAddress: alice.address, contractName: "A", invert: true) } +access(all) fun testStageInvalidHexCodeFails() { + let txResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["Foo", "12309fana9u13nuaerf09adf"], + fooAccount + ) + Test.expect(txResult, Test.beFailed()) + + assertIsStaged(contractAddress: fooAccount.address, contractName: "Foo", invert: true) +} + access(all) fun testStageContractSucceeds() { let txResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", @@ -137,6 +148,15 @@ access(all) fun testStageMultipleContractsSucceeds() { assertStagedContractCodeEqual({ "B": bUpdateCadence, "C": cUpdateCadence }, allStagedCodeForBCAccount) } +access(all) fun testReplaceStagedCodeSucceeds() { + let txResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["Foo", fooUpdateCode], + fooAccount + ) + Test.expect(txResult, Test.beSucceeded()) +} + access(all) fun testUnstageContractSucceeds() { let txResult = executeTransaction( "../transactions/migration-contract-staging/unstage_contract.cdc", From 5c50a3b89be161cb60b2f33f8df75e5e62a08e23 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:19:11 -0600 Subject: [PATCH 24/40] add contract comments --- contracts/MigrationContractStaging.cdc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 7e79c19..dc99b8b 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -220,12 +220,14 @@ access(all) contract MigrationContractStaging { ********************/ /// Serves as identification for a caller's address. - /// NOTE: Should be saved in storage and access safeguarded as reference grants access to contract staging. + /// NOTE: Should be saved in storage and access safeguarded as reference grants access to contract staging. If a + /// contract host wishes to delegate staging to another account (e.g. multisig account setup enabling a developer + /// to stage on its behalf), it should create a PRIVATE Host capability and publish it to the receiving account. /// access(all) resource Host { /// Returns the resource owner's address /// - access(all) view fun address(): Address{ + access(all) view fun address(): Address { return self.owner?.address ?? panic("Host is unowned!") } } From 1a9479c536ae7f744a4704ab612682a93242a1f0 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:24:29 -0600 Subject: [PATCH 25/40] update README --- README.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0bbe258..dd2c393 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Coordinated Upgrade. > here once it has been deployed. | Network | Address | -|---|---| -| Testnet | TBD | -| Mainnet | TBD | +| ------- | ------- | +| Testnet | TBD | +| Mainnet | TBD | ### Pre-Requisites @@ -118,6 +118,100 @@ access(all) fun main(contractAddress: Address, contractName: String): String? { } ``` +## `MigrationContractStaging` Contract Details + +The basic interface to stage a contract is the same as deploying a contract - name + code. See the +[`stage_contract`](./transactions/migration-contract-staging/stage_contract.cdc) & +[`unstage_contract`](./transactions/migration-contract-staging/unstage_contract.cdc) transactions. Note that calling +`stageContract()` again for the same contract will overwrite any existing staged code for that contract. + +```cadence +/// 1 - Create a host and save it in your contract-hosting account at the path derived from deriveHostStoragePath(). +access(all) fun createHost(): @Host +/// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage. +access(all) fun stageContract(host: &Host, name: String, code: String) +/// Removes the staged contract code from the staging environment. +access(all) fun unstageContract(host: &Host, name: String) +``` + +To stage a contract, the developer first saves a `Host` resource in their account which they pass as a reference along +with the contract name and code they wish to stage. The `Host` reference simply serves as proof of authority that the +caller has access to the contract-hosting account, which in the simplest case would be the signer of the staging +transaction, though conceivably this could be delegated to some other account via Capability - possibly helpful for some +multisig contract hosts. + +```cadence +/// Serves as identification for a caller's address. +access(all) resource Host { + /// Returns the resource owner's address + access(all) view fun address(): Address +} +``` + +Within the `MigrationContractStaging` contract account, code is saved on a contract-basis as a `ContractUpdate` struct +within a `Capsule` resource and stored at a the derived path. The `Capsule` simply serves as a dedicated repository for +staged contract code. + +```cadence +/// Represents contract and its corresponding code. +access(all) struct ContractUpdate { + access(all) let address: Address + access(all) let name: String + access(all) var code: String + + /// Validates that the named contract exists at the target address. + access(all) view fun isValid(): Bool + /// Serializes the address and name into a string of the form 0xADDRESS.NAME + access(all) view fun toString(): String + /// Returns human-readable string of the Cadence code. + access(all) view fun codeAsCadence(): String + /// Replaces the ContractUpdate code with that provided. + access(contract) fun replaceCode(_ code: String) +} + +/// Resource that stores pending contract updates in a ContractUpdate struct. +access(all) resource Capsule { + /// The address, name and code of the contract that will be updated. + access(self) let update: ContractUpdate + + /// Returns the staged contract update in the form of a ContractUpdate struct. + access(all) view fun getContractUpdate(): ContractUpdate + /// Replaces the staged contract code with the given hex-encoded Cadence code. + access(contract) fun replaceCode(code: String) +} +``` + +To support ongoing staging progress across the network, the single `StagingStatusUpdated` event is emitted any time a +contract is staged (`status == true`), staged code is replaced (`status == false`), or a contract is unstaged (`status +== nil`). + +```cadence +access(all) event StagingStatusUpdated( + capsuleUUID: UInt64, + address: Address, + codeHash: [UInt8], + contract: String, + status: Bool? +) +``` +Included in the contact are methods for querying staging status and retrieval of staged code. This enables platforms +like Flowview, Flowdiver, ContractBrowser, etc. to display the staging status of contracts on any given account. + +```cadence +/* --- Public Getters --- */ +// +/// Returns true if the contract is currently staged. +access(all) fun isStaged(address: Address, name: String): Bool +/// Returns the names of all staged contracts for the given address. +access(all) fun getStagedContractNames(forAddress: Address): [String] +/// Returns the staged contract Cadence code for the given address and name. +access(all) fun getStagedContractCode(address: Address, name: String): String? +/// Returns an array of all staged contract host addresses. +access(all) view fun getAllStagedContractHosts(): [Address] +/// Returns a dictionary of all staged contract code for the given address. +access(all) fun getAllStagedContractCode(forAddress: Address): {String: String} +``` + ## References More tooling is slated to support Cadence 1.0 code changes and will be added as it arises. For any real-time help, be From 4be1303205d6a47c2be49be89709dae0d71341e8 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:49:43 -0600 Subject: [PATCH 26/40] replace MigrationContractStaging Host path derivation with constant --- README.md | 11 +++++----- contracts/MigrationContractStaging.cdc | 22 +++++-------------- .../stage_contract.cdc | 7 +++--- .../unstage_contract.cdc | 3 +-- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index dd2c393..9f65650 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,11 @@ transaction(contractName: String, contractCode: String) { prepare(signer: AuthAccount) { // Configure Host resource if needed - let hostStoragePath: StoragePath = MigrationContractStaging.deriveHostStoragePath(hostAddress: signer.address) - if signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath) == nil { - signer.save(<-MigrationContractStaging.createHost(), to: hostStoragePath) + if signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) == nil { + signer.save(<-MigrationContractStaging.createHost(), to: MigrationContractStaging.HostStoragePath) } // Assign Host reference - self.host = signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath)! + self.host = signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath)! } execute { @@ -126,7 +125,7 @@ The basic interface to stage a contract is the same as deploying a contract - na `stageContract()` again for the same contract will overwrite any existing staged code for that contract. ```cadence -/// 1 - Create a host and save it in your contract-hosting account at the path derived from deriveHostStoragePath(). +/// 1 - Create a host and save it in your contract-hosting account at MigrationContractStaging.HostStoragePath access(all) fun createHost(): @Host /// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage. access(all) fun stageContract(host: &Host, name: String, code: String) @@ -181,7 +180,7 @@ access(all) resource Capsule { } ``` -To support ongoing staging progress across the network, the single `StagingStatusUpdated` event is emitted any time a +To support monitoring staging progress across the network, the single `StagingStatusUpdated` event is emitted any time a contract is staged (`status == true`), staged code is replaced (`status == false`), or a contract is unstaged (`status == nil`). diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index dc99b8b..4cc3de0 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -12,11 +12,11 @@ /// access(all) contract MigrationContractStaging { - // Path derivation constants + // Path constants // access(self) let delimiter: String - access(self) let hostPathPrefix: String access(self) let capsulePathPrefix: String + access(all) let HostStoragePath: StoragePath /// Maps contract addresses to an array of staged contract names access(self) let stagedContracts: {Address: [String]} @@ -37,7 +37,7 @@ access(all) contract MigrationContractStaging { /* --- Staging Methods --- */ - /// 1 - Create a host and save it in your contract-hosting account at the path derived from deriveHostStoragePath() + /// 1 - Create a host and save it in your contract-hosting account at MigrationContractStaging.HostStoragePath /// /// Creates a Host serving as identification for the contract account. Reference to this resource identifies the /// calling address so it must be saved in account storage before being used to stage a contract update. @@ -147,16 +147,6 @@ access(all) contract MigrationContractStaging { return stagedCode } - /// Returns a StoragePath to store the Host of the form: - /// /storage/self.hostPathPrefix_ADDRESS - access(all) fun deriveHostStoragePath(hostAddress: Address): StoragePath { - return StoragePath( - identifier: self.hostPathPrefix - .concat(self.delimiter) - .concat(hostAddress.toString()) - ) ?? panic("Could not derive Host StoragePath for given address") - } - /// Returns a StoragePath to store the Capsule of the form: /// /storage/self.capsulePathPrefix_ADDRESS_NAME access(all) fun deriveCapsuleStoragePath(contractAddress: Address, contractName: String): StoragePath { @@ -325,9 +315,9 @@ access(all) contract MigrationContractStaging { init() { self.delimiter = "_" - self.hostPathPrefix = "MigrationContractStagingHost".concat(self.delimiter) - .concat(self.delimiter) - .concat(self.account.address.toString()) + self.HostStoragePath = StoragePath( + identifier: "MigrationContractStagingHost".concat(self.delimiter).concat(self.account.address.toString()) + ) ?? panic("Could not derive Host StoragePath") self.capsulePathPrefix = "MigrationContractStagingCapsule" .concat(self.delimiter) .concat(self.account.address.toString()) diff --git a/transactions/migration-contract-staging/stage_contract.cdc b/transactions/migration-contract-staging/stage_contract.cdc index 222e4e8..69bf685 100644 --- a/transactions/migration-contract-staging/stage_contract.cdc +++ b/transactions/migration-contract-staging/stage_contract.cdc @@ -15,12 +15,11 @@ transaction(contractName: String, contractCode: String) { prepare(signer: AuthAccount) { // Configure Host resource if needed - let hostStoragePath: StoragePath = MigrationContractStaging.deriveHostStoragePath(hostAddress: signer.address) - if signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath) == nil { - signer.save(<-MigrationContractStaging.createHost(), to: hostStoragePath) + if signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) == nil { + signer.save(<-MigrationContractStaging.createHost(), to: MigrationContractStaging.HostStoragePath) } // Assign Host reference - self.host = signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath)! + self.host = signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath)! } execute { diff --git a/transactions/migration-contract-staging/unstage_contract.cdc b/transactions/migration-contract-staging/unstage_contract.cdc index d7d2fda..7e8e065 100644 --- a/transactions/migration-contract-staging/unstage_contract.cdc +++ b/transactions/migration-contract-staging/unstage_contract.cdc @@ -10,8 +10,7 @@ transaction(contractName: String) { prepare(signer: AuthAccount) { // Assign Host reference - let hostStoragePath: StoragePath = MigrationContractStaging.deriveHostStoragePath(hostAddress: signer.address) - self.host = signer.borrow<&MigrationContractStaging.Host>(from: hostStoragePath) + self.host = signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) ?? panic("Host was not found in storage") } From 7d2e56a4c942d46fe3ef15f24203cffd8276bf56 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:10:56 -0600 Subject: [PATCH 27/40] add MigrationContractStaging.stagingCutoff field, setter & test cases --- contracts/MigrationContractStaging.cdc | 42 ++++++++- .../get_staging_cutoff.cdc | 7 ++ scripts/test/get_current_block_height.cdc | 3 + tests/migration_contract_staging_tests.cdc | 92 +++++++++++++++++++ .../admin/set_staging_cutoff.cdc | 22 +++++ 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 scripts/migration-contract-staging/get_staging_cutoff.cdc create mode 100644 scripts/test/get_current_block_height.cdc create mode 100644 transactions/migration-contract-staging/admin/set_staging_cutoff.cdc diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 4cc3de0..27232d2 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -17,8 +17,12 @@ access(all) contract MigrationContractStaging { access(self) let delimiter: String access(self) let capsulePathPrefix: String access(all) let HostStoragePath: StoragePath + access(all) let AdminStoragePath: StoragePath /// Maps contract addresses to an array of staged contract names access(self) let stagedContracts: {Address: [String]} + /// The block height at which updates can no no longer be staged. If nil, updates can be staged indefinitely until + /// the cutoff value is set. + access(all) var stagingCutoff: UInt64? /// Event emitted when a contract's code is staged /// status == true - insert | staged == false - replace | staged == nil - remove @@ -30,6 +34,8 @@ access(all) contract MigrationContractStaging { contract: String, status: Bool? ) + /// Emitted when the stagingCutoff value is updated + access(all) event StagingCutoffUpdated(old: UInt64?, new: UInt64?) /******************** Public Methods @@ -52,6 +58,9 @@ access(all) contract MigrationContractStaging { /// the code will be replaced. /// access(all) fun stageContract(host: &Host, name: String, code: String) { + pre { + self.stagingCutoff == nil || self.stagingCutoff! > getCurrentBlock().height: "Staging period has ended" + } let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) if self.stagedContracts[host.address()] == nil { // First time we're seeing contracts from this address - insert the address and contract name @@ -78,6 +87,7 @@ access(all) contract MigrationContractStaging { /// access(all) fun unstageContract(host: &Host, name: String) { pre { + self.stagingCutoff == nil || self.stagingCutoff! > getCurrentBlock().height: "Staging period has ended" self.isStaged(address: host.address(), name: name): "Contract is not staged" } post { @@ -99,13 +109,13 @@ access(all) contract MigrationContractStaging { /// Returns true if the contract is currently staged. /// - access(all) fun isStaged(address: Address, name: String): Bool { + access(all) view fun isStaged(address: Address, name: String): Bool { return self.stagedContracts[address]?.contains(name) ?? false } /// Returns the names of all staged contracts for the given address. /// - access(all) fun getStagedContractNames(forAddress: Address): [String] { + access(all) view fun getStagedContractNames(forAddress: Address): [String] { return self.stagedContracts[forAddress] ?? [] } @@ -128,7 +138,7 @@ access(all) contract MigrationContractStaging { /// Returns a dictionary of all staged contract code for the given address. /// - access(all) fun getAllStagedContractCode(forAddress: Address): {String: String} { + access(all) view fun getAllStagedContractCode(forAddress: Address): {String: String} { if self.stagedContracts[forAddress] == nil { return {} } @@ -149,7 +159,7 @@ access(all) contract MigrationContractStaging { /// Returns a StoragePath to store the Capsule of the form: /// /storage/self.capsulePathPrefix_ADDRESS_NAME - access(all) fun deriveCapsuleStoragePath(contractAddress: Address, contractName: String): StoragePath { + access(all) view fun deriveCapsuleStoragePath(contractAddress: Address, contractName: String): StoragePath { return StoragePath( identifier: self.capsulePathPrefix .concat(self.delimiter) @@ -267,6 +277,26 @@ access(all) contract MigrationContractStaging { } } + /******************** + Admin + ********************/ + + /// Admin resource for updating the stagingCutoff value + /// + access(all) resource Admin { + + /// Sets the block height at which updates can no longer be staged + /// + access(all) fun setStagingCutoff(atHeight: UInt64?) { + pre { + atHeight == nil || atHeight! > getCurrentBlock().height: + "Height must be nil or greater than current block height" + } + emit StagingCutoffUpdated(old: MigrationContractStaging.stagingCutoff, new: atHeight) + MigrationContractStaging.stagingCutoff = atHeight + } + } + /********************* Internal Methods *********************/ @@ -318,9 +348,13 @@ access(all) contract MigrationContractStaging { self.HostStoragePath = StoragePath( identifier: "MigrationContractStagingHost".concat(self.delimiter).concat(self.account.address.toString()) ) ?? panic("Could not derive Host StoragePath") + self.AdminStoragePath = /storage/MigrationContractStagingAdmin self.capsulePathPrefix = "MigrationContractStagingCapsule" .concat(self.delimiter) .concat(self.account.address.toString()) self.stagedContracts = {} + self.stagingCutoff = nil + + self.account.save(<-create Admin(), to: self.AdminStoragePath) } } diff --git a/scripts/migration-contract-staging/get_staging_cutoff.cdc b/scripts/migration-contract-staging/get_staging_cutoff.cdc new file mode 100644 index 0000000..a027253 --- /dev/null +++ b/scripts/migration-contract-staging/get_staging_cutoff.cdc @@ -0,0 +1,7 @@ +import "MigrationContractStaging" + +/// Returns the block height at which contracts can no longer be staged. +/// +access(all) fun main(): UInt64? { + return MigrationContractStaging.stagingCutoff +} diff --git a/scripts/test/get_current_block_height.cdc b/scripts/test/get_current_block_height.cdc new file mode 100644 index 0000000..c21e761 --- /dev/null +++ b/scripts/test/get_current_block_height.cdc @@ -0,0 +1,3 @@ +access(all) fun main(): UInt64 { + return getCurrentBlock().height +} \ No newline at end of file diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc index 42bf804..4b83639 100644 --- a/tests/migration_contract_staging_tests.cdc +++ b/tests/migration_contract_staging_tests.cdc @@ -1,5 +1,6 @@ import Test import BlockchainHelpers +import "MigrationContractStaging" // Contract hosts as defined in flow.json access(all) let fooAccount = Test.getAccount(0x0000000000000008) @@ -16,6 +17,9 @@ access(all) let bUpdateCadence = String.fromUTF8(bUpdateCode.decodeHex()) ?? pan access(all) let cUpdateCode = "696d706f727420412066726f6d203078303030303030303030303030303030390a696d706f727420422066726f6d203078303030303030303030303030303031300a0a61636365737328616c6c2920636f6e74726163742043207b0a0a2020202061636365737328616c6c29206c65742053746f72616765506174683a2053746f72616765506174680a2020202061636365737328616c6c29206c6574205075626c6963506174683a205075626c6963506174680a0a2020202061636365737328616c6c29207265736f7572636520696e74657266616365204f757465725075626c6963207b0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e670a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e670a202020207d0a0a2020202061636365737328616c6c29207265736f75726365204f75746572203a204f757465725075626c6963207b0a202020202020202061636365737328616c6c29206c657420696e6e65723a20407b55496e7436343a20412e527d0a0a2020202020202020696e69742829207b0a20202020202020202020202073656c662e696e6e6572203c2d207b7d0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20676574466f6f46726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e666f6f2829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2067657442617246726f6d2869643a2055496e743634293a20537472696e67207b0a20202020202020202020202072657475726e2073656c662e626f72726f775265736f75726365286964293f2e6261722829203f3f2070616e696328224e6f207265736f7572636520666f756e64207769746820676976656e20494422290a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e206164645265736f75726365285f20693a2040412e5229207b0a20202020202020202020202073656c662e696e6e65725b692e757569645d203c2d2120690a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e20626f72726f775265736f75726365285f2069643a2055496e743634293a20267b412e497d3f207b0a20202020202020202020202072657475726e202673656c662e696e6e65725b69645d20617320267b412e497d3f0a20202020202020207d0a0a202020202020202061636365737328616c6c292066756e2072656d6f76655265736f75726365285f2069643a2055496e743634293a2040412e523f207b0a20202020202020202020202072657475726e203c2d2073656c662e696e6e65722e72656d6f7665286b65793a206964290a20202020202020207d0a0a202020202020202064657374726f792829207b0a20202020202020202020202064657374726f792073656c662e696e6e65720a20202020202020207d0a202020207d0a0a20202020696e69742829207b0a202020202020202073656c662e53746f7261676550617468203d202f73746f726167652f4f757465720a202020202020202073656c662e5075626c696350617468203d202f7075626c69632f4f757465725075626c69630a0a202020202020202073656c662e6163636f756e742e736176653c404f757465723e283c2d637265617465204f7574657228292c20746f3a2073656c662e53746f7261676550617468290a202020202020202073656c662e6163636f756e742e6c696e6b3c267b4f757465725075626c69637d3e2873656c662e5075626c6963506174682c207461726765743a2073656c662e53746f7261676550617468290a0a20202020202020206c6574206f75746572203d2073656c662e6163636f756e742e626f72726f773c264f757465723e2866726f6d3a2073656c662e53746f726167655061746829210a20202020202020206f757465722e6164645265736f75726365283c2d20422e637265617465522829290a202020207d0a7d" access(all) let cUpdateCadence = String.fromUTF8(cUpdateCode.decodeHex()) ?? panic("Problem decoding cUpdateCode") +// Block height different to add to the staging cutoff +access(all) let blockHeightDelta: UInt64 = 10 + access(all) fun setup() { var err = Test.deployContract( name: "MigrationContractStaging", @@ -97,6 +101,14 @@ access(all) fun testStageContractSucceeds() { let allStagedCodeForFooAccount = getAllStagedContractCodeForAddress(contractAddress: fooAccount.address) assertStagedContractCodeEqual({ "Foo": fooUpdateCadence}, allStagedCodeForFooAccount) + + let events = Test.eventsOfType(Type()) + Test.assertEqual(1, events.length) + + let evt = events[0] as! MigrationContractStaging.StagingStatusUpdated + Test.assertEqual(fooAccount.address, evt.address) + Test.assertEqual("Foo", evt.contract) + Test.assertEqual(true, evt.status!) } access(all) fun testStageMultipleContractsSucceeds() { @@ -124,6 +136,21 @@ access(all) fun testStageMultipleContractsSucceeds() { assertIsStaged(contractAddress: bcAccount.address, contractName: "B", invert: false) assertIsStaged(contractAddress: bcAccount.address, contractName: "C", invert: false) + let events = Test.eventsOfType(Type()) + Test.assertEqual(4, events.length) + let cEvt = events[1] as! MigrationContractStaging.StagingStatusUpdated + Test.assertEqual(bcAccount.address, cEvt.address) + Test.assertEqual("C", cEvt.contract) + Test.assertEqual(true, cEvt.status!) + let bEvt = events[2] as! MigrationContractStaging.StagingStatusUpdated + Test.assertEqual(bcAccount.address, cEvt.address) + Test.assertEqual("B", bEvt.contract) + Test.assertEqual(true, bEvt.status!) + let aEvt = events[3] as! MigrationContractStaging.StagingStatusUpdated + Test.assertEqual(aAccount.address, aEvt.address) + Test.assertEqual("A", aEvt.contract) + Test.assertEqual(true, aEvt.status!) + let aAccountStagedContractNames = getStagedContractNamesForAddress(aAccount.address) let bcAccountStagedContractNames = getStagedContractNamesForAddress(bcAccount.address) Test.assert(aAccountStagedContractNames.length == 1, message: "Invalid number of staged contracts on aAccount") @@ -155,6 +182,13 @@ access(all) fun testReplaceStagedCodeSucceeds() { fooAccount ) Test.expect(txResult, Test.beSucceeded()) + + let events = Test.eventsOfType(Type()) + Test.assertEqual(5, events.length) + let evt = events[4] as! MigrationContractStaging.StagingStatusUpdated + Test.assertEqual(fooAccount.address, evt.address) + Test.assertEqual("Foo", evt.contract) + Test.assertEqual(false, evt.status!) } access(all) fun testUnstageContractSucceeds() { @@ -165,6 +199,13 @@ access(all) fun testUnstageContractSucceeds() { ) Test.expect(txResult, Test.beSucceeded()) + let events = Test.eventsOfType(Type()) + Test.assertEqual(6, events.length) + let evt = events[5] as! MigrationContractStaging.StagingStatusUpdated + Test.assertEqual(fooAccount.address, evt.address) + Test.assertEqual("Foo", evt.contract) + Test.assertEqual(nil, evt.status) + assertIsStaged(contractAddress: fooAccount.address, contractName: "Foo", invert: true) let fooAccountStagedContractNames = getStagedContractNamesForAddress(fooAccount.address) @@ -180,6 +221,46 @@ access(all) fun testUnstageContractSucceeds() { assertStagedContractCodeEqual({}, allStagedCodeForFooAccount) } +access(all) fun testSetStagingCutoffSucceeds() { + let admin = Test.getAccount(0x07) + let currentHeight = executeScript( + "../scripts/test/get_current_block_height.cdc", + [] + ).returnValue as! UInt64? ?? panic("Problem retrieving current block height") + let expectedCutoff: UInt64 = currentHeight + blockHeightDelta + let txResult = executeTransaction( + "../transactions/migration-contract-staging/admin/set_staging_cutoff.cdc", + [expectedCutoff], + admin + ) + Test.expect(txResult, Test.beSucceeded()) + + let events = Test.eventsOfType(Type()) + Test.assertEqual(1, events.length) + let evt = events[0] as! MigrationContractStaging.StagingCutoffUpdated + Test.assertEqual(nil, evt.old) + Test.assertEqual(expectedCutoff, evt.new!) + + let stagingCutoffResult = executeScript( + "../scripts/migration-contract-staging/get_staging_cutoff.cdc", + [] + ) + let stagingCutoff = stagingCutoffResult.returnValue as! UInt64? ?? panic("Problem retrieving staging cutoff value") + Test.assertEqual(expectedCutoff, stagingCutoff) + + tickTock(advanceBlocks: blockHeightDelta + 1, admin) +} + +access(all) fun testStageBeyondCutoffFails() { + + let stageAttemptResult = executeTransaction( + "../transactions/migration-contract-staging/stage_contract.cdc", + ["Foo"], + fooAccount + ) + Test.expect(stageAttemptResult, Test.beFailed()) +} + /* --- Test Helpers --- */ access(all) fun assertIsStaged(contractAddress: Address, contractName: String, invert: Bool) { @@ -242,4 +323,15 @@ access(all) fun getAllStagedContractCodeForAddress(contractAddress: Address): {S ) return allStagedContractCodeForAddressResult.returnValue as! {String: String}? ?? panic("Problem retrieving result of getAllStagedContractCodeForAddress()") +} + +access(all) fun tickTock(advanceBlocks: UInt64, _ signer: Test.Account) { + var blocksAdvanced: UInt64 = 0 + while blocksAdvanced < advanceBlocks { + + let txResult = executeTransaction("../transactions/test/tick_tock.cdc", [], signer) + Test.expect(txResult, Test.beSucceeded()) + + blocksAdvanced = blocksAdvanced + 1 + } } \ No newline at end of file diff --git a/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc b/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc new file mode 100644 index 0000000..9070473 --- /dev/null +++ b/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc @@ -0,0 +1,22 @@ +import "MigrationContractStaging" + +/// Sets the block height at which contracts can no longer be staged +/// +transaction(cutoff: UInt64) { + + let admin: &MigrationContractStaging.Admin + + prepare(signer: AuthAccount) { + self.admin = signer.borrow<&MigrationContractStaging.Admin>(from: MigrationContractStaging.AdminStoragePath) + ?? panic("Could not borrow Admin reference") + } + + execute { + self.admin.setStagingCutoff(atHeight: cutoff) + } + + post { + MigrationContractStaging.stagingCutoff == cutoff: + "Staging cutoff was not set properly" + } +} From 484ca2b4eeff12e3efb8629fe9e6610c3bdb6323 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:17:59 -0600 Subject: [PATCH 28/40] Apply suggestions from code review Co-authored-by: Joshua Hannan --- README.md | 2 +- contracts/MigrationContractStaging.cdc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f65650..755c482 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ transaction(contractName: String, contractCode: String) { ``` At the end of this transaction, your contract will be staged in the `MigrationContractStaging` account. If you staged -this contract's code previously, it will be overwritted by the code you provided in this transaction. +this contract's code previously, it will be overwritten by the code you provided in this transaction. > :warning: NOTE: Staging your contract successfully does not mean that your contract code is correct. Your testing and > validation processes should include testing your contract code against the Cadence 1.0 interpreter to ensure your diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 27232d2..83e4f3e 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -76,7 +76,7 @@ access(all) contract MigrationContractStaging { ?? panic("Could not borrow existing Capsule from storage for staged contract") capsule.replaceCode(code: code) } else { - // First time staging this contrac - add the contract name to the list of contracts staged for host + // First time staging this contract - add the contract name to the list of contracts staged for host self.stagedContracts[host.address()]!.append(name) self.account.save(<-self.createCapsule(host: host, name: name, code: code), to: capsulePath) } From 60af686df486dc512f0b283388cd0697b53edd58 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:22:11 -0600 Subject: [PATCH 29/40] make MigrationContractStaging.unstageContract no-op if non-existent --- contracts/MigrationContractStaging.cdc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 83e4f3e..34001c8 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -88,12 +88,14 @@ access(all) contract MigrationContractStaging { access(all) fun unstageContract(host: &Host, name: String) { pre { self.stagingCutoff == nil || self.stagingCutoff! > getCurrentBlock().height: "Staging period has ended" - self.isStaged(address: host.address(), name: name): "Contract is not staged" } post { !self.isStaged(address: host.address(), name: name): "Contract is still staged" } let address: Address = host.address() + if self.stagedContracts[address] == nil { + return + } let capsuleUUID: UInt64 = self.removeStagedContract(address: address, name: name) ?? panic("Problem destroying update Capsule") emit StagingStatusUpdated( From c02a4355d5912eef289e73cbf7e16ce530f29dcf Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:30:36 -0600 Subject: [PATCH 30/40] replace Python util with hex-encode.sh --- README.md | 8 ++------ hex-encode.sh | 11 +++++++++++ src/get_code_hex.py | 14 -------------- 3 files changed, 13 insertions(+), 20 deletions(-) create mode 100755 hex-encode.sh delete mode 100644 src/get_code_hex.py diff --git a/README.md b/README.md index 755c482..5bc9ce8 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,8 @@ Coordinated Upgrade. - A Cadence 1.0 compatible contract serving as an update to your existing contract. Extending our example, if you're staging `A` in address `0x01`, you should have a contract named `A` that is Cadence 1.0 compatible. See the references below for more information on Cadence 1.0 language changes. -- Your contract as a hex string. - - Included in this repo is a Python util to hex-encode your contract which outputs your contract's code as a hex - string. With Python installed, run: - ```sh - python3 ./src/get_code_hex.py - ``` +- Your contract as a hex string. You can get this by running `./hex-encode.sh ` - **be sure to + explicitly state your contract's import addresses!** ### Staging Your Contract Update diff --git a/hex-encode.sh b/hex-encode.sh new file mode 100755 index 0000000..bab6d5e --- /dev/null +++ b/hex-encode.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Check if exactly one argument is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Hex encode the file's contents +cat "$1" | xxd -p | tr -d '\n' + diff --git a/src/get_code_hex.py b/src/get_code_hex.py deleted file mode 100644 index 894fe5b..0000000 --- a/src/get_code_hex.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys - -def get_code_hex(contract_path): - with open(contract_path, "r") as contract_file: - return bytes(contract_file.read(), "UTF-8").hex() - -def main(): - if len(sys.argv) != 2: - print("Usage: python3 get_code_hex.py ") - sys.exit() - print(get_code_hex(sys.argv[1])) - -if __name__ == "__main__": - main() \ No newline at end of file From 171f5071576dbcbe820f7f631ff2debe5977c361 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:38:11 -0600 Subject: [PATCH 31/40] impl @bjartek's feedback - update MigrationContractStaging.StagingStatusUpdated event & .stageContract() --- contracts/MigrationContractStaging.cdc | 34 +++++++++++----------- tests/migration_contract_staging_tests.cdc | 12 ++++---- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 34001c8..e2fcbeb 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -25,14 +25,14 @@ access(all) contract MigrationContractStaging { access(all) var stagingCutoff: UInt64? /// Event emitted when a contract's code is staged - /// status == true - insert | staged == false - replace | staged == nil - remove + /// `action` ∈ {"stage", "replace", "unstage"} each denoting the action being taken on the staged contract /// NOTE: Does not guarantee that the contract code is valid Cadence access(all) event StagingStatusUpdated( capsuleUUID: UInt64, address: Address, codeHash: [UInt8], contract: String, - status: Bool? + action: String ) /// Emitted when the stagingCutoff value is updated access(all) event StagingCutoffUpdated(old: UInt64?, new: UInt64?) @@ -68,19 +68,19 @@ access(all) contract MigrationContractStaging { // Create a new Capsule to store the staged code let capsule: @Capsule <- self.createCapsule(host: host, name: name, code: code) self.account.save(<-capsule, to: capsulePath) - } else { - // We've seen contracts from this host address before - if let contractIndex: Int = self.stagedContracts[host.address()]!.firstIndex(of: name) { - // The contract is already staged - replace the code - let capsule: &Capsule = self.account.borrow<&Capsule>(from: capsulePath) - ?? panic("Could not borrow existing Capsule from storage for staged contract") - capsule.replaceCode(code: code) - } else { - // First time staging this contract - add the contract name to the list of contracts staged for host - self.stagedContracts[host.address()]!.append(name) - self.account.save(<-self.createCapsule(host: host, name: name, code: code), to: capsulePath) - } + return + } + // We've seen contracts from this host address before - check if the contract is already staged + if let contractIndex: Int = self.stagedContracts[host.address()]!.firstIndex(of: name) { + // The contract is already staged - replace the code + let capsule: &Capsule = self.account.borrow<&Capsule>(from: capsulePath) + ?? panic("Could not borrow existing Capsule from storage for staged contract") + capsule.replaceCode(code: code) + return } + // First time staging this contract - add the contract name to the list of contracts staged for host + self.stagedContracts[host.address()]!.append(name) + self.account.save(<-self.createCapsule(host: host, name: name, code: code), to: capsulePath) } /// Removes the staged contract code from the staging environment. @@ -103,7 +103,7 @@ access(all) contract MigrationContractStaging { address: address, codeHash: [], contract: name, - status: nil + action: "unstage" ) } @@ -274,7 +274,7 @@ access(all) contract MigrationContractStaging { address: self.update.address, codeHash: code.decodeHex(), contract: self.update.name, - status: false + action: "replace" ) } } @@ -314,7 +314,7 @@ access(all) contract MigrationContractStaging { address: host.address(), codeHash: code.decodeHex(), contract: name, - status: true + action: "stage" ) return <- capsule } diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc index 4b83639..a5def12 100644 --- a/tests/migration_contract_staging_tests.cdc +++ b/tests/migration_contract_staging_tests.cdc @@ -108,7 +108,7 @@ access(all) fun testStageContractSucceeds() { let evt = events[0] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(fooAccount.address, evt.address) Test.assertEqual("Foo", evt.contract) - Test.assertEqual(true, evt.status!) + Test.assertEqual("stage", evt.action) } access(all) fun testStageMultipleContractsSucceeds() { @@ -141,15 +141,15 @@ access(all) fun testStageMultipleContractsSucceeds() { let cEvt = events[1] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(bcAccount.address, cEvt.address) Test.assertEqual("C", cEvt.contract) - Test.assertEqual(true, cEvt.status!) + Test.assertEqual("stage", cEvt.action) let bEvt = events[2] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(bcAccount.address, cEvt.address) Test.assertEqual("B", bEvt.contract) - Test.assertEqual(true, bEvt.status!) + Test.assertEqual("stage", bEvt.action) let aEvt = events[3] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(aAccount.address, aEvt.address) Test.assertEqual("A", aEvt.contract) - Test.assertEqual(true, aEvt.status!) + Test.assertEqual("stage", aEvt.action) let aAccountStagedContractNames = getStagedContractNamesForAddress(aAccount.address) let bcAccountStagedContractNames = getStagedContractNamesForAddress(bcAccount.address) @@ -188,7 +188,7 @@ access(all) fun testReplaceStagedCodeSucceeds() { let evt = events[4] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(fooAccount.address, evt.address) Test.assertEqual("Foo", evt.contract) - Test.assertEqual(false, evt.status!) + Test.assertEqual("replace", evt.action) } access(all) fun testUnstageContractSucceeds() { @@ -204,7 +204,7 @@ access(all) fun testUnstageContractSucceeds() { let evt = events[5] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(fooAccount.address, evt.address) Test.assertEqual("Foo", evt.contract) - Test.assertEqual(nil, evt.status) + Test.assertEqual("unstage", evt.action) assertIsStaged(contractAddress: fooAccount.address, contractName: "Foo", invert: true) From 0c707bf67d0e4bec0b22e4344d7259f45f8e93ff Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:42:13 -0600 Subject: [PATCH 32/40] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5bc9ce8..54c3351 100644 --- a/README.md +++ b/README.md @@ -177,8 +177,8 @@ access(all) resource Capsule { ``` To support monitoring staging progress across the network, the single `StagingStatusUpdated` event is emitted any time a -contract is staged (`status == true`), staged code is replaced (`status == false`), or a contract is unstaged (`status -== nil`). +contract is staged (`status == "stage"`), staged code is replaced (`status == "replace"`), or a contract is unstaged +(`status == "unstage"`). ```cadence access(all) event StagingStatusUpdated( @@ -186,7 +186,7 @@ access(all) event StagingStatusUpdated( address: Address, codeHash: [UInt8], contract: String, - status: Bool? + action: String ) ``` Included in the contact are methods for querying staging status and retrieval of staged code. This enables platforms From c502c5f9825c7d134e5f93b37b9f2b5b38a8ab0a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Sat, 3 Feb 2024 13:29:56 -0600 Subject: [PATCH 33/40] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bastian Müller --- contracts/MigrationContractStaging.cdc | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index e2fcbeb..7620c84 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -6,7 +6,7 @@ /// /// To stage your contract update: /// 1. create a Host & save in your contract-hosting account -/// 2. call stageContract() passing a reference to you Host, the contract name, and the hex-encoded Cadence code +/// 2. call stageContract() passing a reference to you Host, the contract name, and the updated Cadence code /// /// This can be done in a single transaction! For more code context, see https://github.com/onflow/contract-updater /// @@ -59,19 +59,19 @@ access(all) contract MigrationContractStaging { /// access(all) fun stageContract(host: &Host, name: String, code: String) { pre { - self.stagingCutoff == nil || self.stagingCutoff! > getCurrentBlock().height: "Staging period has ended" + self.stagingCutoff == nil || getCurrentBlock().height <= self.stagingCutoff!: "Staging period has ended" } - let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) + let capsulePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) if self.stagedContracts[host.address()] == nil { // First time we're seeing contracts from this address - insert the address and contract name self.stagedContracts.insert(key: host.address(), [name]) // Create a new Capsule to store the staged code - let capsule: @Capsule <- self.createCapsule(host: host, name: name, code: code) + let capsule <- self.createCapsule(host: host, name: name, code: code) self.account.save(<-capsule, to: capsulePath) return } // We've seen contracts from this host address before - check if the contract is already staged - if let contractIndex: Int = self.stagedContracts[host.address()]!.firstIndex(of: name) { + if let contractIndex = self.stagedContracts[host.address()]!.firstIndex(of: name) { // The contract is already staged - replace the code let capsule: &Capsule = self.account.borrow<&Capsule>(from: capsulePath) ?? panic("Could not borrow existing Capsule from storage for staged contract") @@ -162,13 +162,13 @@ access(all) contract MigrationContractStaging { /// Returns a StoragePath to store the Capsule of the form: /// /storage/self.capsulePathPrefix_ADDRESS_NAME access(all) view fun deriveCapsuleStoragePath(contractAddress: Address, contractName: String): StoragePath { - return StoragePath( - identifier: self.capsulePathPrefix - .concat(self.delimiter) - .concat(contractAddress.toString()) - .concat(self.delimiter) - .concat(contractName) - ) ?? panic("Could not derive Capsule StoragePath for given address") + let identifier = self.capsulePathPrefix + .concat(self.delimiter) + .concat(contractAddress.toString()) + .concat(self.delimiter) + .concat(contractName) + return StoragePath(identifier: identifier) + ?? panic("Could not derive Capsule StoragePath for given address") } /* ------------------------------------------------------------------------------------------------------------ */ @@ -262,7 +262,7 @@ access(all) contract MigrationContractStaging { return self.update } - /// Replaces the staged contract code with the given hex-encoded Cadence code. + /// Replaces the staged contract code with the given updated Cadence code. /// access(contract) fun replaceCode(code: String) { post { @@ -289,13 +289,13 @@ access(all) contract MigrationContractStaging { /// Sets the block height at which updates can no longer be staged /// - access(all) fun setStagingCutoff(atHeight: UInt64?) { + access(all) fun setStagingCutoff(at height: UInt64?) { pre { - atHeight == nil || atHeight! > getCurrentBlock().height: + height == nil || height! > getCurrentBlock().height: "Height must be nil or greater than current block height" } - emit StagingCutoffUpdated(old: MigrationContractStaging.stagingCutoff, new: atHeight) - MigrationContractStaging.stagingCutoff = atHeight + emit StagingCutoffUpdated(old: MigrationContractStaging.stagingCutoff, new: height) + MigrationContractStaging.stagingCutoff = height } } From d01818edba78c640586359bfd2e81e4a8e8ffc34 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Sat, 3 Feb 2024 13:45:38 -0600 Subject: [PATCH 34/40] update MigrationContractStaging.stagingCutoff access & conditions with getters --- contracts/MigrationContractStaging.cdc | 18 +++++++++++++++--- .../get_staging_cutoff.cdc | 2 +- .../admin/set_staging_cutoff.cdc | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 7620c84..d5563d1 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -22,7 +22,7 @@ access(all) contract MigrationContractStaging { access(self) let stagedContracts: {Address: [String]} /// The block height at which updates can no no longer be staged. If nil, updates can be staged indefinitely until /// the cutoff value is set. - access(all) var stagingCutoff: UInt64? + access(self) var stagingCutoff: UInt64? /// Event emitted when a contract's code is staged /// `action` ∈ {"stage", "replace", "unstage"} each denoting the action being taken on the staged contract @@ -59,7 +59,7 @@ access(all) contract MigrationContractStaging { /// access(all) fun stageContract(host: &Host, name: String, code: String) { pre { - self.stagingCutoff == nil || getCurrentBlock().height <= self.stagingCutoff!: "Staging period has ended" + self.isActiveStagingPeriod(): "Staging period has ended" } let capsulePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) if self.stagedContracts[host.address()] == nil { @@ -87,7 +87,7 @@ access(all) contract MigrationContractStaging { /// access(all) fun unstageContract(host: &Host, name: String) { pre { - self.stagingCutoff == nil || self.stagingCutoff! > getCurrentBlock().height: "Staging period has ended" + self.isActiveStagingPeriod(): "Staging period has ended" } post { !self.isStaged(address: host.address(), name: name): "Contract is still staged" @@ -109,6 +109,18 @@ access(all) contract MigrationContractStaging { /* --- Public Getters --- */ + /// Returns the last block height at which updates can be staged + /// + access(all) fun getStagingCutoff(): UInt64? { + return self.stagingCutoff + } + + /// Returns whether the staging period is currently active + /// + access(all) fun isActiveStagingPeriod(): Bool { + return self.stagingCutoff == nil || getCurrentBlock().height <= self.stagingCutoff! + } + /// Returns true if the contract is currently staged. /// access(all) view fun isStaged(address: Address, name: String): Bool { diff --git a/scripts/migration-contract-staging/get_staging_cutoff.cdc b/scripts/migration-contract-staging/get_staging_cutoff.cdc index a027253..3c7371a 100644 --- a/scripts/migration-contract-staging/get_staging_cutoff.cdc +++ b/scripts/migration-contract-staging/get_staging_cutoff.cdc @@ -3,5 +3,5 @@ import "MigrationContractStaging" /// Returns the block height at which contracts can no longer be staged. /// access(all) fun main(): UInt64? { - return MigrationContractStaging.stagingCutoff + return MigrationContractStaging.getStagingCutoff() } diff --git a/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc b/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc index 9070473..6c67efb 100644 --- a/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc +++ b/transactions/migration-contract-staging/admin/set_staging_cutoff.cdc @@ -12,11 +12,11 @@ transaction(cutoff: UInt64) { } execute { - self.admin.setStagingCutoff(atHeight: cutoff) + self.admin.setStagingCutoff(at: cutoff) } post { - MigrationContractStaging.stagingCutoff == cutoff: + MigrationContractStaging.getStagingCutoff() == cutoff: "Staging cutoff was not set properly" } } From d107b21ebf38717bebaa2e6f7624c0f43127f06b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Sat, 3 Feb 2024 13:51:05 -0600 Subject: [PATCH 35/40] remove explicit type declarations in MigrationContractStaging --- contracts/MigrationContractStaging.cdc | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index d5563d1..8b6e5eb 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -59,7 +59,7 @@ access(all) contract MigrationContractStaging { /// access(all) fun stageContract(host: &Host, name: String, code: String) { pre { - self.isActiveStagingPeriod(): "Staging period has ended" + self.isStagingPeriodActive(): "Staging period has ended" } let capsulePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name) if self.stagedContracts[host.address()] == nil { @@ -73,7 +73,7 @@ access(all) contract MigrationContractStaging { // We've seen contracts from this host address before - check if the contract is already staged if let contractIndex = self.stagedContracts[host.address()]!.firstIndex(of: name) { // The contract is already staged - replace the code - let capsule: &Capsule = self.account.borrow<&Capsule>(from: capsulePath) + let capsule = self.account.borrow<&Capsule>(from: capsulePath) ?? panic("Could not borrow existing Capsule from storage for staged contract") capsule.replaceCode(code: code) return @@ -87,16 +87,16 @@ access(all) contract MigrationContractStaging { /// access(all) fun unstageContract(host: &Host, name: String) { pre { - self.isActiveStagingPeriod(): "Staging period has ended" + self.isStagingPeriodActive(): "Staging period has ended" } post { !self.isStaged(address: host.address(), name: name): "Contract is still staged" } - let address: Address = host.address() + let address = host.address() if self.stagedContracts[address] == nil { return } - let capsuleUUID: UInt64 = self.removeStagedContract(address: address, name: name) + let capsuleUUID = self.removeStagedContract(address: address, name: name) ?? panic("Problem destroying update Capsule") emit StagingStatusUpdated( capsuleUUID: capsuleUUID, @@ -117,7 +117,7 @@ access(all) contract MigrationContractStaging { /// Returns whether the staging period is currently active /// - access(all) fun isActiveStagingPeriod(): Bool { + access(all) fun isStagingPeriodActive(): Bool { return self.stagingCutoff == nil || getCurrentBlock().height <= self.stagingCutoff! } @@ -136,7 +136,7 @@ access(all) contract MigrationContractStaging { /// Returns the staged contract Cadence code for the given address and name. /// access(all) fun getStagedContractCode(address: Address, name: String): String? { - let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) + let capsulePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) if let capsule = self.account.borrow<&Capsule>(from: capsulePath) { return capsule.getContractUpdate().codeAsCadence() } else { @@ -153,18 +153,18 @@ access(all) contract MigrationContractStaging { /// Returns a dictionary of all staged contract code for the given address. /// access(all) view fun getAllStagedContractCode(forAddress: Address): {String: String} { - if self.stagedContracts[forAddress] == nil { + let contractNames = self.stagedContracts[forAddress] + if contractNames == nil { return {} } let capsulePaths: [StoragePath] = [] let stagedCode: {String: String} = {} - let contractNames: [String] = self.stagedContracts[forAddress]! - for name in contractNames { + for name in contractNames! { capsulePaths.append(self.deriveCapsuleStoragePath(contractAddress: forAddress, contractName: name)) } for path in capsulePaths { if let capsule = self.account.borrow<&Capsule>(from: path) { - let update: ContractUpdate = capsule.getContractUpdate() + let update = capsule.getContractUpdate() stagedCode[update.name] = update.codeAsCadence() } } @@ -335,7 +335,7 @@ access(all) contract MigrationContractStaging { /// wasn't found. Also removes the contract name from the stagedContracts mapping. /// access(self) fun removeStagedContract(address: Address, name: String): UInt64? { - let contractIndex: Int = self.stagedContracts[address]!.firstIndex(of: name)! + let contractIndex = self.stagedContracts[address]!.firstIndex(of: name)! self.stagedContracts[address]!.remove(at: contractIndex) // Remove the Address from the stagedContracts mapping if it has no staged contracts remain for the host address if self.stagedContracts[address]!.length == 0 { @@ -348,7 +348,7 @@ access(all) contract MigrationContractStaging { /// the destroyed Capsule if it existed. /// access(self) fun destroyCapsule(address: Address, name: String): UInt64? { - let capsulePath: StoragePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) + let capsulePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) if let capsule <- self.account.load<@Capsule>(from: capsulePath) { let capsuleUUID = capsule.uuid destroy capsule From 7ad772ddbc346304f0a0ee14946fd5b6e24d2eab Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Sat, 3 Feb 2024 14:54:15 -0600 Subject: [PATCH 36/40] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bastian Müller --- transactions/migration-contract-staging/stage_contract.cdc | 2 +- transactions/migration-contract-staging/unstage_contract.cdc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/transactions/migration-contract-staging/stage_contract.cdc b/transactions/migration-contract-staging/stage_contract.cdc index 69bf685..d5daa5f 100644 --- a/transactions/migration-contract-staging/stage_contract.cdc +++ b/transactions/migration-contract-staging/stage_contract.cdc @@ -8,7 +8,7 @@ import "MigrationContractStaging" /// For more context, see the repo - https://github.com/onflow/contract-updater /// /// @param contractName: The name of the contract to be updated with the given code -/// @param contractCode: The updated contract code as a hex-encoded String +/// @param contractCode: The updated contract code /// transaction(contractName: String, contractCode: String) { let host: &MigrationContractStaging.Host diff --git a/transactions/migration-contract-staging/unstage_contract.cdc b/transactions/migration-contract-staging/unstage_contract.cdc index 7e8e065..1482f79 100644 --- a/transactions/migration-contract-staging/unstage_contract.cdc +++ b/transactions/migration-contract-staging/unstage_contract.cdc @@ -20,7 +20,7 @@ transaction(contractName: String) { } post { - MigrationContractStaging.isStaged(address: self.host.address(), name: contractName) == false: + !MigrationContractStaging.isStaged(address: self.host.address(), name: contractName): "Problem while unstaging update" } } From 26216bae653c4c9ae9f3cdecccd4e2ef247bc8fd Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Sat, 3 Feb 2024 15:11:40 -0600 Subject: [PATCH 37/40] refactor MigrationContractStaging to remove use of hex-encoded strings --- README.md | 10 +++---- contracts/MigrationContractStaging.cdc | 22 +++++---------- hex-encode.sh | 11 -------- tests/migration_contract_staging_tests.cdc | 31 +++++++--------------- 4 files changed, 20 insertions(+), 54 deletions(-) delete mode 100755 hex-encode.sh diff --git a/README.md b/README.md index 54c3351..ebc16b7 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,16 @@ Coordinated Upgrade. - A Cadence 1.0 compatible contract serving as an update to your existing contract. Extending our example, if you're staging `A` in address `0x01`, you should have a contract named `A` that is Cadence 1.0 compatible. See the references below for more information on Cadence 1.0 language changes. -- Your contract as a hex string. You can get this by running `./hex-encode.sh ` - **be sure to - explicitly state your contract's import addresses!** ### Staging Your Contract Update Armed with your pre-requisites, you're ready to stage your contract update. Simply run the [`stage_contract.cdc` -transaction](./transactions/migration-contract-staging/stage_contract.cdc), passing your contract's name and hex string -as arguments and signing as the contract host account. +transaction](./transactions/migration-contract-staging/stage_contract.cdc), passing your contract's name and Cadence +code as arguments and signing as the contract host account. ```sh flow transactions send ./transactions/migration-contract-staging/stage_contract.cdc \ - \ + "$(cat )" \ --signer \ --network ``` @@ -171,7 +169,7 @@ access(all) resource Capsule { /// Returns the staged contract update in the form of a ContractUpdate struct. access(all) view fun getContractUpdate(): ContractUpdate - /// Replaces the staged contract code with the given hex-encoded Cadence code. + /// Replaces the staged contract code with the given Cadence code. access(contract) fun replaceCode(code: String) } ``` diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 8b6e5eb..5b53579 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -30,7 +30,7 @@ access(all) contract MigrationContractStaging { access(all) event StagingStatusUpdated( capsuleUUID: UInt64, address: Address, - codeHash: [UInt8], + codeUTF8: [UInt8], contract: String, action: String ) @@ -101,7 +101,7 @@ access(all) contract MigrationContractStaging { emit StagingStatusUpdated( capsuleUUID: capsuleUUID, address: address, - codeHash: [], + codeUTF8: [], contract: name, action: "unstage" ) @@ -138,7 +138,7 @@ access(all) contract MigrationContractStaging { access(all) fun getStagedContractCode(address: Address, name: String): String? { let capsulePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name) if let capsule = self.account.borrow<&Capsule>(from: capsulePath) { - return capsule.getContractUpdate().codeAsCadence() + return capsule.getContractUpdate().code } else { return nil } @@ -165,7 +165,7 @@ access(all) contract MigrationContractStaging { for path in capsulePaths { if let capsule = self.account.borrow<&Capsule>(from: path) { let update = capsule.getContractUpdate() - stagedCode[update.name] = update.codeAsCadence() + stagedCode[update.name] = update.code } } return stagedCode @@ -216,12 +216,6 @@ access(all) contract MigrationContractStaging { return self.address.toString().concat(".").concat(self.name) } - /// Returns human-readable string of the Cadence code. - /// - access(all) view fun codeAsCadence(): String { - return String.fromUTF8(self.code.decodeHex()) ?? panic("Problem stringifying code!") - } - /// Replaces the ContractUpdate code with that provided. /// access(contract) fun replaceCode(_ code: String) { @@ -262,7 +256,6 @@ access(all) contract MigrationContractStaging { init(update: ContractUpdate) { pre { - update.codeAsCadence() != nil: "Staged update code must be valid Cadence" update.isValid(): "Target contract does not exist" } self.update = update @@ -277,14 +270,11 @@ access(all) contract MigrationContractStaging { /// Replaces the staged contract code with the given updated Cadence code. /// access(contract) fun replaceCode(code: String) { - post { - self.update.codeAsCadence() != nil: "Staged update code must be valid Cadence" - } self.update.replaceCode(code) emit StagingStatusUpdated( capsuleUUID: self.uuid, address: self.update.address, - codeHash: code.decodeHex(), + codeUTF8: code.utf8, contract: self.update.name, action: "replace" ) @@ -324,7 +314,7 @@ access(all) contract MigrationContractStaging { emit StagingStatusUpdated( capsuleUUID: capsule.uuid, address: host.address(), - codeHash: code.decodeHex(), + codeUTF8: code.utf8, contract: name, action: "stage" ) diff --git a/hex-encode.sh b/hex-encode.sh deleted file mode 100755 index bab6d5e..0000000 --- a/hex-encode.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# Check if exactly one argument is provided -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -# Hex encode the file's contents -cat "$1" | xxd -p | tr -d '\n' - diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc index a5def12..54d1c32 100644 --- a/tests/migration_contract_staging_tests.cdc +++ b/tests/migration_contract_staging_tests.cdc @@ -61,28 +61,17 @@ access(all) fun testStagedNonExistentContractFails() { let alice = Test.createAccount() let txResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", - ["A", aUpdateCode], + ["A", aUpdateCadence], alice ) Test.expect(txResult, Test.beFailed()) assertIsStaged(contractAddress: alice.address, contractName: "A", invert: true) } -access(all) fun testStageInvalidHexCodeFails() { - let txResult = executeTransaction( - "../transactions/migration-contract-staging/stage_contract.cdc", - ["Foo", "12309fana9u13nuaerf09adf"], - fooAccount - ) - Test.expect(txResult, Test.beFailed()) - - assertIsStaged(contractAddress: fooAccount.address, contractName: "Foo", invert: true) -} - access(all) fun testStageContractSucceeds() { let txResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", - ["Foo", fooUpdateCode], + ["Foo", fooUpdateCadence], fooAccount ) Test.expect(txResult, Test.beSucceeded()) @@ -97,7 +86,7 @@ access(all) fun testStageContractSucceeds() { let fooStagedContractCode = getStagedContractCode(contractAddress: fooAccount.address, contractName: "Foo") ?? panic("Problem retrieving result of getStagedContractCode()") - Test.assertEqual(fooUpdateCode, String.encodeHex(fooStagedContractCode.utf8)) + Test.assertEqual(fooUpdateCadence, fooStagedContractCode) let allStagedCodeForFooAccount = getAllStagedContractCodeForAddress(contractAddress: fooAccount.address) assertStagedContractCodeEqual({ "Foo": fooUpdateCadence}, allStagedCodeForFooAccount) @@ -115,19 +104,19 @@ access(all) fun testStageMultipleContractsSucceeds() { // Demonstrating staging multiple contracts on the same host & out of dependency order let cStagingTxResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", - ["C", cUpdateCode], + ["C", cUpdateCadence], bcAccount ) Test.expect(cStagingTxResult, Test.beSucceeded()) let bStagingTxResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", - ["B", bUpdateCode], + ["B", bUpdateCadence], bcAccount ) Test.expect(bStagingTxResult, Test.beSucceeded()) let aStagingTxResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", - ["A", aUpdateCode], + ["A", aUpdateCadence], aAccount ) Test.expect(aStagingTxResult, Test.beSucceeded()) @@ -165,9 +154,9 @@ access(all) fun testStageMultipleContractsSucceeds() { ?? panic("Problem retrieving result of getStagedContractCode()") let cStagedCode = getStagedContractCode(contractAddress: bcAccount.address, contractName: "C") ?? panic("Problem retrieving result of getStagedContractCode()") - Test.assertEqual(aUpdateCode, String.encodeHex(aStagedCode.utf8)) - Test.assertEqual(bUpdateCode, String.encodeHex(bStagedCode.utf8)) - Test.assertEqual(cUpdateCode, String.encodeHex(cStagedCode.utf8)) + Test.assertEqual(aUpdateCadence, aStagedCode) + Test.assertEqual(bUpdateCadence, bStagedCode) + Test.assertEqual(cUpdateCadence, cStagedCode) let allStagedCodeForAAccount = getAllStagedContractCodeForAddress(contractAddress: aAccount.address) let allStagedCodeForBCAccount = getAllStagedContractCodeForAddress(contractAddress: bcAccount.address) @@ -178,7 +167,7 @@ access(all) fun testStageMultipleContractsSucceeds() { access(all) fun testReplaceStagedCodeSucceeds() { let txResult = executeTransaction( "../transactions/migration-contract-staging/stage_contract.cdc", - ["Foo", fooUpdateCode], + ["Foo", fooUpdateCadence], fooAccount ) Test.expect(txResult, Test.beSucceeded()) From f2eeb19494e6e351e01a4bded4c21aed5d4db895 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:06:02 -0600 Subject: [PATCH 38/40] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bastian Müller --- contracts/MigrationContractStaging.cdc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index 5b53579..bec51c9 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -30,7 +30,7 @@ access(all) contract MigrationContractStaging { access(all) event StagingStatusUpdated( capsuleUUID: UInt64, address: Address, - codeUTF8: [UInt8], + code: String, contract: String, action: String ) @@ -101,7 +101,7 @@ access(all) contract MigrationContractStaging { emit StagingStatusUpdated( capsuleUUID: capsuleUUID, address: address, - codeUTF8: [], + code: "", contract: name, action: "unstage" ) @@ -274,7 +274,7 @@ access(all) contract MigrationContractStaging { emit StagingStatusUpdated( capsuleUUID: self.uuid, address: self.update.address, - codeUTF8: code.utf8, + code: code, contract: self.update.name, action: "replace" ) @@ -314,7 +314,7 @@ access(all) contract MigrationContractStaging { emit StagingStatusUpdated( capsuleUUID: capsule.uuid, address: host.address(), - codeUTF8: code.utf8, + code: code, contract: name, action: "stage" ) From 9110c7276881965d18cd4054e152459bf4dac6bd Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:20:19 -0600 Subject: [PATCH 39/40] add coverage for MigrationContractStaging.StagingStatusUpdated.code values --- tests/migration_contract_staging_tests.cdc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/migration_contract_staging_tests.cdc b/tests/migration_contract_staging_tests.cdc index 54d1c32..d156710 100644 --- a/tests/migration_contract_staging_tests.cdc +++ b/tests/migration_contract_staging_tests.cdc @@ -96,6 +96,7 @@ access(all) fun testStageContractSucceeds() { let evt = events[0] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(fooAccount.address, evt.address) + Test.assertEqual(fooUpdateCadence, evt.code) Test.assertEqual("Foo", evt.contract) Test.assertEqual("stage", evt.action) } @@ -129,14 +130,17 @@ access(all) fun testStageMultipleContractsSucceeds() { Test.assertEqual(4, events.length) let cEvt = events[1] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(bcAccount.address, cEvt.address) + Test.assertEqual(cUpdateCadence, cEvt.code) Test.assertEqual("C", cEvt.contract) Test.assertEqual("stage", cEvt.action) let bEvt = events[2] as! MigrationContractStaging.StagingStatusUpdated - Test.assertEqual(bcAccount.address, cEvt.address) + Test.assertEqual(bcAccount.address, bEvt.address) + Test.assertEqual(bUpdateCadence, bEvt.code) Test.assertEqual("B", bEvt.contract) Test.assertEqual("stage", bEvt.action) let aEvt = events[3] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(aAccount.address, aEvt.address) + Test.assertEqual(aUpdateCadence, aEvt.code) Test.assertEqual("A", aEvt.contract) Test.assertEqual("stage", aEvt.action) @@ -176,6 +180,7 @@ access(all) fun testReplaceStagedCodeSucceeds() { Test.assertEqual(5, events.length) let evt = events[4] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(fooAccount.address, evt.address) + Test.assertEqual(fooUpdateCadence, evt.code) Test.assertEqual("Foo", evt.contract) Test.assertEqual("replace", evt.action) } @@ -192,6 +197,7 @@ access(all) fun testUnstageContractSucceeds() { Test.assertEqual(6, events.length) let evt = events[5] as! MigrationContractStaging.StagingStatusUpdated Test.assertEqual(fooAccount.address, evt.address) + Test.assertEqual("", evt.code) Test.assertEqual("Foo", evt.contract) Test.assertEqual("unstage", evt.action) From 9551380ac600f8b6ab1c2f0fe96b77b01c7f98c8 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:32:39 -0600 Subject: [PATCH 40/40] update MigrationContractStaging comments --- contracts/MigrationContractStaging.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/MigrationContractStaging.cdc b/contracts/MigrationContractStaging.cdc index bec51c9..a58dd11 100644 --- a/contracts/MigrationContractStaging.cdc +++ b/contracts/MigrationContractStaging.cdc @@ -24,7 +24,7 @@ access(all) contract MigrationContractStaging { /// the cutoff value is set. access(self) var stagingCutoff: UInt64? - /// Event emitted when a contract's code is staged + /// Event emitted when a contract's code is staged, replaced or unstaged /// `action` ∈ {"stage", "replace", "unstage"} each denoting the action being taken on the staged contract /// NOTE: Does not guarantee that the contract code is valid Cadence access(all) event StagingStatusUpdated(