Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contract supporting migration staging path #14

Merged
merged 40 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fc5a63b
update blockUpdateBoundary logic for unknown boundary contract init
sisyphusSmiling Dec 16, 2023
12c2eb9
add MigrationContractStaging contract supporting 1.0 migration
sisyphusSmiling Jan 24, 2024
6b1fb68
Expand on UpdaterCreated event data
sisyphusSmiling Jan 24, 2024
e73e244
add deriveUpdaterPublicPath method
sisyphusSmiling Jan 24, 2024
d246278
add stage & unstage contract transactions
sisyphusSmiling Jan 24, 2024
17a987d
add migration supporting scripts
sisyphusSmiling Jan 25, 2024
1a10aca
fix Updater setup bug in MigrationContractStaging
sisyphusSmiling Jan 25, 2024
3603d0f
update setup comments
sisyphusSmiling Jan 25, 2024
6e9f8d2
fix MigrationContractStaging.ContractUpdate.verify
sisyphusSmiling Jan 25, 2024
dc2c672
add contract comments + rename statusCheck to systems check
sisyphusSmiling Jan 25, 2024
631df33
refactor MigrationContractStaging according to migration needs
sisyphusSmiling Jan 25, 2024
d69f40e
refactor migration transactions & scripts
sisyphusSmiling Jan 25, 2024
7a31e20
add helper scripts & update comments
sisyphusSmiling Jan 25, 2024
3d850a2
update MigrationContractStaging.getStagedContractNames() return type
sisyphusSmiling Jan 25, 2024
f0fe3a6
revert StagedContractUpdates related changes
sisyphusSmiling Jan 25, 2024
561944b
revert StagedContractUpdates related transaction
sisyphusSmiling Jan 25, 2024
92bf719
restructure repo for emphasis on Cadence 1.0 staging
sisyphusSmiling Jan 26, 2024
c17eaec
update README
sisyphusSmiling Jan 26, 2024
64fb140
add contract comments
sisyphusSmiling Jan 26, 2024
e4657ea
add initial MigrationContractStaging tests
sisyphusSmiling Jan 26, 2024
47b03ad
fix MigrationContractStaging.stageContract bug
sisyphusSmiling Jan 26, 2024
ddbed96
add MigrationContractStaging test coverage
sisyphusSmiling Jan 26, 2024
d5de4de
add MigrationContractStaging test cases
sisyphusSmiling Jan 26, 2024
5c50a3b
add contract comments
sisyphusSmiling Jan 26, 2024
1a9479c
update README
sisyphusSmiling Jan 26, 2024
4be1303
replace MigrationContractStaging Host path derivation with constant
sisyphusSmiling Jan 29, 2024
7d2e56a
add MigrationContractStaging.stagingCutoff field, setter & test cases
sisyphusSmiling Jan 29, 2024
484ca2b
Apply suggestions from code review
sisyphusSmiling Jan 29, 2024
60af686
make MigrationContractStaging.unstageContract no-op if non-existent
sisyphusSmiling Jan 29, 2024
c02a435
replace Python util with hex-encode.sh
sisyphusSmiling Jan 29, 2024
171f507
impl @bjartek's feedback - update MigrationContractStaging.StagingSta…
sisyphusSmiling Jan 31, 2024
0c707bf
update README
sisyphusSmiling Jan 31, 2024
c502c5f
Apply suggestions from code review
sisyphusSmiling Feb 3, 2024
d01818e
update MigrationContractStaging.stagingCutoff access & conditions wit…
sisyphusSmiling Feb 3, 2024
d107b21
remove explicit type declarations in MigrationContractStaging
sisyphusSmiling Feb 3, 2024
7ad772d
Apply suggestions from code review
sisyphusSmiling Feb 3, 2024
26216ba
refactor MigrationContractStaging to remove use of hex-encoded strings
sisyphusSmiling Feb 3, 2024
f2eeb19
Apply suggestions from code review
sisyphusSmiling Feb 5, 2024
9110c72
add coverage for MigrationContractStaging.StagingStatusUpdated.code v…
sisyphusSmiling Feb 5, 2024
9551380
update MigrationContractStaging comments
sisyphusSmiling Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 181 additions & 181 deletions README.md

Large diffs are not rendered by default.

364 changes: 364 additions & 0 deletions contracts/MigrationContractStaging.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
/// This contract is intended for use in the Cadence 1.0 contract migration across the Flow network.
///
/// 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.
///
/// 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 updated 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 constants
//
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(self) var stagingCutoff: UInt64?

/// 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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧡

capsuleUUID: UInt64,
address: Address,
code: String,
contract: String,
action: String
sisyphusSmiling marked this conversation as resolved.
Show resolved Hide resolved
)
/// Emitted when the stagingCutoff value is updated
access(all) event StagingCutoffUpdated(old: UInt64?, new: UInt64?)

/********************
Public Methods
********************/

/* --- Staging Methods --- */

/// 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.
///
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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this guard against using a host that is not saved? Or is it not possible to get a ref to a host with it not beeing saved?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the Host must be saved as host.address() panics if owner == nil

pre {
self.isStagingPeriodActive(): "Staging period has ended"
}
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 <- self.createCapsule(host: host, name: name, code: code)
self.account.save(<-capsule, to: capsulePath)
sisyphusSmiling marked this conversation as resolved.
Show resolved Hide resolved
return
}
// 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 = 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.
///
access(all) fun unstageContract(host: &Host, name: String) {
pre {
self.isStagingPeriodActive(): "Staging period has ended"
}
post {
!self.isStaged(address: host.address(), name: name): "Contract is still staged"
}
let address = host.address()
if self.stagedContracts[address] == nil {
return
}
let capsuleUUID = self.removeStagedContract(address: address, name: name)
?? panic("Problem destroying update Capsule")
emit StagingStatusUpdated(
capsuleUUID: capsuleUUID,
address: address,
code: "",
contract: name,
action: "unstage"
)
}

/* --- 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 isStagingPeriodActive(): 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 {
return self.stagedContracts[address]?.contains(name) ?? false
}

/// Returns the names of all staged contracts for the given address.
///
access(all) view 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 = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name)
if let capsule = self.account.borrow<&Capsule>(from: capsulePath) {
return capsule.getContractUpdate().code
} 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) view fun getAllStagedContractCode(forAddress: Address): {String: String} {
let contractNames = self.stagedContracts[forAddress]
if contractNames == nil {
return {}
}
let capsulePaths: [StoragePath] = []
let stagedCode: {String: String} = {}
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 = capsule.getContractUpdate()
stagedCode[update.name] = update.code
}
}
return stagedCode
}

/// 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 {
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")
}

/* ------------------------------------------------------------------------------------------------------------ */
/* ------------------------------------------------ Constructs ------------------------------------------------ */
/* ------------------------------------------------------------------------------------------------------------ */

/********************
ContractUpdate
********************/

/// 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

init(address: Address, name: String, code: String) {
self.address = address
self.name = name
self.code = code
}

/// 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 of the form 0xADDRESS.NAME
///
access(all) view fun toString(): String {
return self.address.toString().concat(".").concat(self.name)
}

/// Replaces the ContractUpdate code with that provided.
///
access(contract) fun replaceCode(_ code: String) {
self.code = code
}
}

/********************
Host
********************/

/// Serves as identification for a caller's address.
/// 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 {
return self.owner?.address ?? panic("Host is unowned!")
}
joshuahannan marked this conversation as resolved.
Show resolved Hide resolved
}

/********************
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 Capsule {
/// The address, name and code of the contract that will be updated.
access(self) let update: ContractUpdate

init(update: ContractUpdate) {
pre {
update.isValid(): "Target contract does not exist"
}
self.update = update
}

/// 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 updated Cadence code.
///
access(contract) fun replaceCode(code: String) {
self.update.replaceCode(code)
emit StagingStatusUpdated(
capsuleUUID: self.uuid,
address: self.update.address,
code: code,
contract: self.update.name,
action: "replace"
)
}
}

/********************
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(at height: UInt64?) {
pre {
height == nil || height! > getCurrentBlock().height:
"Height must be nil or greater than current block height"
}
emit StagingCutoffUpdated(old: MigrationContractStaging.stagingCutoff, new: height)
MigrationContractStaging.stagingCutoff = height
}
}

/*********************
Internal Methods
*********************/

/// 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(),
code: code,
contract: name,
action: "stage"
)
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 = 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)
}

/// 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 = 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.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)
}
}
Loading
Loading