Skip to content

Commit

Permalink
Merge pull request #6 from onflow/stable-cadence-refactor
Browse files Browse the repository at this point in the history
Stable cadence refactor
  • Loading branch information
sisyphusSmiling authored Jun 11, 2024
2 parents 735a84b + a1a49cd commit c99c1a9
Show file tree
Hide file tree
Showing 57 changed files with 1,819 additions and 1,157 deletions.
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ name: CI

on:
pull_request:
branches: [main]
branches:
- main
- stable-cadence
push:
branches: [main]

branches:
- main
- stable-cadence
jobs:
tests:
name: Flow CLI Tests
Expand All @@ -15,25 +18,22 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.20.x'
go-version: "1.20.x"
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install Flow CLI
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.13.1
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/feature/stable-cadence/install.sh)"
- name: Flow CLI Version
run: flow version
run: flow-c1 version
- name: Update PATH
run: echo "/root/.local/bin" >> $GITHUB_PATH
- name: Run tests
run: make ci
- name: Normalize coverage report filepaths
run : sh ./local/normalize_coverage_report.sh
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
test:
$(MAKE) generate -C lib/go
$(MAKE) test -C lib/go
flow test --cover --covercode="contracts" --coverprofile="coverage.lcov" tests/*.cdc
flow-c1 test --cover --covercode="contracts" --coverprofile="coverage.lcov" tests/*.cdc

.PHONY: ci
ci:
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ Coordinated Upgrade.
| Network | Address |
| --------- | ------------------------------------------------------------------------------------------------------------------------- |
| Crescendo | [0x27b2302520211b67](https://crescendo.flowdiver.io/contract/A.27b2302520211b67.MigrationContractStaging?tab=deployments) |
| Testnet | [0x2ceae959ed1a7e7a](https://contractbrowser.com/A.2ceae959ed1a7e7a.MigrationContractStaging) |
| Mainnet | [56100d46aa9b0212](https://contractbrowser.com/A.56100d46aa9b0212.MigrationContractStaging) |

Expand Down Expand Up @@ -106,13 +105,13 @@ import "MigrationContractStaging"
transaction(contractName: String, contractCode: String) {
let host: &MigrationContractStaging.Host
prepare(signer: AuthAccount) {
prepare(signer: auth(BorrowValue, SaveValue) &Account) {
// Configure Host resource if needed
if signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) == nil {
signer.save(<-MigrationContractStaging.createHost(), to: MigrationContractStaging.HostStoragePath)
if signer.storage.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) == nil {
signer.storage.save(<-MigrationContractStaging.createHost(), to: MigrationContractStaging.HostStoragePath)
}
// Assign Host reference
self.host = signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath)!
self.host = signer.storage.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath)!
}
execute {
Expand Down Expand Up @@ -229,7 +228,7 @@ access(all) struct ContractUpdate {
access(all) view fun identifier(): String
/// Returns whether this contract update passed the last emulated migration, validating the contained code.
/// NOTE: false could mean validation hasn't begun, the code wasn't included in emulation, or validation failed
access(all) view fun isValidated(): Bool {
access(all) view fun isValidated(): Bool
/// Replaces the ContractUpdate code with that provided.
access(contract) fun replaceCode(_ code: String)
}
Expand All @@ -255,7 +254,7 @@ access(all) event StagingStatusUpdated(
capsuleUUID: UInt64,
address: Address,
code: String,
contract: String,
contractIdentifier: String,
action: String
)
```
Expand Down
130 changes: 130 additions & 0 deletions contracts/DependencyAudit.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import "MigrationContractStaging"

// This contract is is used by the FVM calling the `checkDependencies` function from a function of the same name and singnature in the FlowServiceAccount contract,
// at the end of every transaction.
// The `dependenciesAddresses` and `dependenciesNames` will be all the dependencies needded to run that transaction.
//
// The `checkDependencies` function will check if any of the dependencies are not staged in the MigrationContractStaging contract.
// If any of the dependencies are not staged, the function will emit an event with the unstaged dependencies, or panic if `panicOnUnstaged` is set to true.
access(all) contract DependencyAudit {

access(all) let AdministratorStoragePath: StoragePath

// The system addresses have contracts that will not be stages via the migration contract so we exclude them from the dependency chekcs
access(self) var excludedAddresses: {Address: Bool}

access(all) var panicOnUnstaged: Bool

access(all) event UnstagedDependencies(dependencies: [Dependency])

access(all) event PanicOnUnstagedDependenciesChanged(shouldPanic: Bool)

// checkDependencies is called from the FlowServiceAccount contract
access(account) fun checkDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) {
var unstagedDependencies: [Dependency] = []

var numDependencies = dependenciesAddresses.length
var i = 0
while i < numDependencies {
let isExcluded = DependencyAudit.excludedAddresses[dependenciesAddresses[i]] ?? false
if isExcluded {
i = i + 1
continue
}

let staged = MigrationContractStaging.isStaged(address: dependenciesAddresses[i], name: dependenciesNames[i])
if !staged {
unstagedDependencies.append(Dependency(address: dependenciesAddresses[i], name: dependenciesNames[i]))
}

i = i + 1
}

if unstagedDependencies.length > 0 {
if DependencyAudit.panicOnUnstaged {
// If `panicOnUnstaged` is set to true, the transaction will panic if there are any unstaged dependencies
// the panic message will include the unstaged dependencies
var unstagedDependenciesString = ""
var numUnstagedDependencies = unstagedDependencies.length
var j = 0
while j < numUnstagedDependencies {
if j > 0 {
unstagedDependenciesString = unstagedDependenciesString.concat(", ")
}
unstagedDependenciesString = unstagedDependenciesString.concat(unstagedDependencies[j].toString())

j = j + 1
}

// the transactions will fail with a message that looks like this: `error: panic: Found unstaged dependencies: A.2ceae959ed1a7e7a.MigrationContractStaging, A.2ceae959ed1a7e7a.DependencyAudit`
panic("This transaction is using dependencies not staged for Crescendo upgrade coming soon! Learn more: https://bit.ly/FLOWCRESCENDO. Dependencies not staged: ".concat(unstagedDependenciesString))
} else {
emit UnstagedDependencies(dependencies: unstagedDependencies)
}
}
}

// The Administrator resorce can be used to add or remove addresses from the excludedAddresses dictionary
//
access(all) resource Administrator {
// addExcludedAddresses add the addresses to the excludedAddresses dictionary
access(all) fun addExcludedAddresses(addresses: [Address]) {
for address in addresses {
DependencyAudit.excludedAddresses[address] = true
}
}

// removeExcludedAddresses remove the addresses from the excludedAddresses dictionary
access(all) fun removeExcludedAddresses(addresses: [Address]) {
for address in addresses {
DependencyAudit.excludedAddresses.remove(key: address)
}
}

// setPanicOnUnstagedDependencies sets the `panicOnUnstaged` variable to the value of `shouldPanic`
access(all) fun setPanicOnUnstagedDependencies(shouldPanic: Bool) {
DependencyAudit.panicOnUnstaged = shouldPanic
emit PanicOnUnstagedDependenciesChanged(shouldPanic: shouldPanic)
}

// testCheckDependencies is used for testing purposes
// It will call the `checkDependencies` function with the provided dependencies
// `checkDependencies` is otherwise not callable from the outside
access(all) fun testCheckDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) {
return DependencyAudit.checkDependencies(dependenciesAddresses, dependenciesNames, authorizers)
}
}

access(all) struct Dependency {
access(all) let address: Address
access(all) let name: String

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

access(all) fun toString(): String {
var addressString = self.address.toString()
// remove 0x prefix
addressString = addressString.slice(from: 2, upTo: addressString.length)
return "A.".concat(addressString).concat(".").concat(self.name)
}
}

// The admin resource is saved to the storage so that the admin can be accessed by the service account
// The `excludedAddresses` will be the addresses with the system contracts.
init(excludedAddresses: [Address]) {
self.excludedAddresses = {}
self.panicOnUnstaged = false

self.AdministratorStoragePath = /storage/flowDependencyAuditAdmin

for address in excludedAddresses {
self.excludedAddresses[address] = true
}

let admin <- create Administrator()
self.account.storage.save(<-admin, to: self.AdministratorStoragePath)
}
}
24 changes: 12 additions & 12 deletions contracts/MigrationContractStaging.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ access(all) contract MigrationContractStaging {
capsuleUUID: UInt64,
address: Address,
code: String,
contract: String,
contractIdentifier: String,
action: String
)
/// Emitted when emulated contract migrations have been completed, where failedContracts are named by their
Expand Down Expand Up @@ -78,20 +78,20 @@ access(all) contract MigrationContractStaging {
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)
self.account.storage.save(<-capsule, to: capsulePath)
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)
let capsule = self.account.storage.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)
self.account.storage.save(<-self.createCapsule(host: host, name: name, code: code), to: capsulePath)
}

/// Removes the staged contract code from the staging environment.
Expand All @@ -113,7 +113,7 @@ access(all) contract MigrationContractStaging {
capsuleUUID: capsuleUUID,
address: address,
code: "",
contract: name,
contractIdentifier: name,
action: "unstage"
)
}
Expand All @@ -122,13 +122,13 @@ access(all) contract MigrationContractStaging {

/// Returns the last block height at which updates can be staged
///
access(all) fun getStagingCutoff(): UInt64? {
access(all) view fun getStagingCutoff(): UInt64? {
return self.stagingCutoff
}

/// Returns whether the staging period is currently active
///
access(all) fun isStagingPeriodActive(): Bool {
access(all) view fun isStagingPeriodActive(): Bool {
return self.stagingCutoff == nil || getCurrentBlock().height <= self.stagingCutoff!
}

Expand Down Expand Up @@ -160,7 +160,7 @@ access(all) contract MigrationContractStaging {
///
access(all) view fun getStagedContractUpdate(address: Address, name: String): ContractUpdate? {
let capsulePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name)
if let capsule = self.account.borrow<&Capsule>(from: capsulePath) {
if let capsule = self.account.storage.borrow<&Capsule>(from: capsulePath) {
return capsule.getContractUpdate()
} else {
return nil
Expand Down Expand Up @@ -353,7 +353,7 @@ access(all) contract MigrationContractStaging {
capsuleUUID: self.uuid,
address: self.update.address,
code: code,
contract: self.update.name,
contractIdentifier: self.update.name,
action: "replace"
)
}
Expand Down Expand Up @@ -407,7 +407,7 @@ access(all) contract MigrationContractStaging {
capsuleUUID: capsule.uuid,
address: host.address(),
code: code,
contract: name,
contractIdentifier: name,
action: "stage"
)
return <- capsule
Expand All @@ -431,7 +431,7 @@ access(all) contract MigrationContractStaging {
///
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) {
if let capsule <- self.account.storage.load<@Capsule>(from: capsulePath) {
let capsuleUUID = capsule.uuid
destroy capsule
return capsuleUUID
Expand All @@ -452,6 +452,6 @@ access(all) contract MigrationContractStaging {
self.stagingCutoff = nil
self.lastEmulatedMigrationResult = nil

self.account.save(<-create Admin(), to: self.AdminStoragePath)
self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
}
}
8 changes: 4 additions & 4 deletions contracts/staged-contract-updates/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# StagedContractUpdates

> :warning: This contract is general purpose and is not supporting Cadence 1.0 contract staging! Please refer to the
> :warning: This contract is general purpose and is not supporting Cadence 1.0 contract staging! Please refer to
> `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
Expand Down Expand Up @@ -93,7 +93,7 @@ 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)
![flat dependency dag](../../resources/dependency_dag.png)

So the contracts should be updated in the following order:

Expand All @@ -114,11 +114,11 @@ More concretely, if we try to update all three contracts in the same transaction
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)
![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)
![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:
Expand Down
Loading

0 comments on commit c99c1a9

Please sign in to comment.