Skip to content

Commit

Permalink
feat(exec): add basic execution flow (sphinx-labs#44)
Browse files Browse the repository at this point in the history
Updates the ChugSplash executor service to include the basic execution
flow. Many components are missing and need to be added to smart
contracts before this will work, but the foundation is there.
  • Loading branch information
smartcontracts authored Aug 20, 2022
1 parent efccd1a commit 4c73fc1
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-colts-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chugsplash/executor': minor
---

Updates ChugSplash executor to include basic execution flow.
13 changes: 13 additions & 0 deletions packages/executor/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// We use hardhat for compiling Solidity contracts. We could use other tools for doing this
// compilation, but hardhat was a simple solution. We should probably replace this with a simpler
// solution later and put the compilation function in @chugsplash/core.

import { HardhatUserConfig } from 'hardhat/types'

const config: HardhatUserConfig = {
solidity: {
version: '0.8.15',
},
}

export default config
7 changes: 6 additions & 1 deletion packages/executor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"url": "https://github.com/smartcontracts/chugsplash.git"
},
"devDependencies": {
"@eth-optimism/common-ts": "^0.6.0"
"@chugsplash/contracts": "^0.1.1",
"@chugsplash/core": "^0.1.0",
"@eth-optimism/common-ts": "^0.6.0",
"@eth-optimism/core-utils": "^0.9.3",
"ethers": "^5.6.8",
"hardhat": "^2.10.0"
}
}
182 changes: 182 additions & 0 deletions packages/executor/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { BaseServiceV2, validators } from '@eth-optimism/common-ts'
import { ethers } from 'ethers'
import {
ChugSplashRegistryABI,
ChugSplashManagerABI,
} from '@chugsplash/contracts'

import {
parseStrategyString,
ExecutorSelectionStrategy,
compileRemoteBundle,
} from './utils'

type Options = {
registry: string
rpc: ethers.providers.StaticJsonRpcProvider
key: string
ess: string
eps: string
}

type Metrics = {}

type State = {
registry: ethers.Contract
wallet: ethers.Wallet
ess: string[]
eps: string[]
}

export class ChugSplashExecutor extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options>) {
super({
name: 'executor',
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
loop: true,
loopIntervalMs: 1000,
options,
optionsSpec: {
registry: {
desc: 'address of the ChugSplashRegistry contract',
validator: validators.str,
},
rpc: {
desc: 'rpc for the chain to run the executor on',
validator: validators.staticJsonRpcProvider,
},
key: {
desc: 'private key to use for signing transactions',
validator: validators.str,
},
ess: {
desc: 'comma separated list of ESS contracts to accept',
validator: validators.str,
},
eps: {
desc: 'comma separated list of EPS contracts to accept',
validator: validators.str,
},
},
metricsSpec: {},
})
}

async init() {
this.state.registry = new ethers.Contract(
this.options.registry,
ChugSplashRegistryABI,
this.options.rpc
)
this.state.wallet = new ethers.Wallet(this.options.key, this.options.rpc)
this.state.ess = parseStrategyString(this.options.ess)
this.state.eps = parseStrategyString(this.options.eps)
}

async main() {
// Put TODOs here
// TODO: recover if we crashed and are in the middle of executing an upgrade

// Find all active upgrades that have not yet been started
const approvalAnnouncementEvents = await this.state.registry.queryFilter(
this.state.registry.filters.EventAnnounced('ChugSplashBundleApproved')
)

for (const approvalAnnouncementEvent of approvalAnnouncementEvents) {
const manager = new ethers.Contract(
approvalAnnouncementEvent.args.manager,
ChugSplashManagerABI,
this.state.wallet
)

// TODO: Add this to the ChugSplashManager contract
const ess = await manager.getExecutorSelectionStrategy()
if (!this.state.ess.includes(ess)) {
// We don't like this strategy, skip the upgrade.
continue
}

// TODO: Add this to the ChugSplashManager contract
const eps = await manager.getExecutorPaymentStrategy()
if (!this.state.eps.includes(eps)) {
// We don't like this strategy, skip the upgrade.
continue
}

const receipt = await approvalAnnouncementEvent.getTransactionReceipt()
const approvalEvent = manager.parseLog(
receipt.logs.find((log) => {
return log.logIndex === approvalAnnouncementEvent.logIndex - 1
})
)

const activeBundleId = await manager.activeBundleId()
if (activeBundleId !== approvalEvent.args.bundleId) {
// This is not the active bundle, so we can skip it.
continue
}

// TODO: Add this to the ChugSplashManager contract
const selectedExecutor = await manager.getSelectedExecutor(activeBundleId)
if (selectedExecutor !== ethers.constants.AddressZero) {
// Someone else has been selected to execute the upgrade, so we can skip it.
continue
}

const proposalEvents = await manager.queryFilter(
manager.filters.EventProposed(activeBundleId)
)
if (proposalEvents.length !== 1) {
// TODO: throw an error here or skip
}

const proposalEvent = proposalEvents[0]
const bundle = await compileRemoteBundle(proposalEvent.args.configUri)
if (bundle.root !== proposalEvent.args.bundleRoot) {
// TODO: throw an error here or skip
}

// Try to become the selected executor.
// TODO: Use an adapter system to make this easier.
if (ess === ExecutorSelectionStrategy.SIMPLE_LOCK) {
try {
const strategy = new ethers.Contract(
ess,
// TODO: Use the right ABI here
ChugSplashManagerABI,
this.state.wallet
)

const tx = await strategy.claim(activeBundleId)
await tx.wait()
} catch (err) {
// Unable to claim the lock, so skip this upgrade.
continue
}
} else {
throw new Error(`unsupported strategy: ${ess}`)
}

// TODO: Handle cancellation cleanly
for (const action of bundle.actions) {
// TODO: Handle errors cleanly
const tx = await manager.executeChugSplashBundleAction(
action.action,
action.proof.actionIndex,
action.proof.siblings
)
await tx.wait()
}

const completedEvents = await manager.queryFilter(
manager.filters.ChugSplashBundleCompleted(activeBundleId)
)
if (completedEvents.length !== 1) {
// TODO: throw an error here
}

// TODO: Check that we got paid appropriately.
}
}
}
69 changes: 69 additions & 0 deletions packages/executor/src/utils/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import hre from 'hardhat'
import { SolcBuild } from 'hardhat/types'
import {
TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD,
TASK_COMPILE_SOLIDITY_RUN_SOLCJS,
TASK_COMPILE_SOLIDITY_RUN_SOLC,
} from 'hardhat/builtin-tasks/task-names'
import { add0x } from '@eth-optimism/core-utils'
import {
CanonicalChugSplashConfig,
ChugSplashActionBundle,
makeActionBundleFromConfig,
} from '@chugsplash/core'

/**
* Compiles a remote ChugSplashBundle from a uri.
*
* @param uri URI of the ChugSplashBundle to compile.
* @returns Compiled ChugSplashBundle.
*/
export const compileRemoteBundle = async (
uri: string
): Promise<ChugSplashActionBundle> => {
let config: CanonicalChugSplashConfig
if (uri.startsWith('ipfs://')) {
config = await (
await fetch(
`https://cloudflare-ipfs.com/ipfs/${uri.replace('ipfs://', '')}`
)
).json()
} else {
throw new Error('unsupported URI type')
}

const artifacts = {}
for (const source of config.inputs) {
const solcBuild: SolcBuild = await hre.run(
TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD,
{
quiet: true,
solcVersion: source.solcVersion,
}
)

let output: any // TODO: Compiler output
if (solcBuild.isSolcJs) {
output = await hre.run(TASK_COMPILE_SOLIDITY_RUN_SOLCJS, {
input: source.input,
solcJsPath: solcBuild.compilerPath,
})
} else {
output = await hre.run(TASK_COMPILE_SOLIDITY_RUN_SOLC, {
input: source.input,
solcPath: solcBuild.compilerPath,
})
}

for (const fileOutput of Object.values(output.contracts)) {
for (const [contractName, contractOutput] of Object.entries(fileOutput)) {
artifacts[contractName] = {
bytecode: add0x(contractOutput.evm.bytecode.object),
storageLayout: contractOutput.storageLayout,
}
}
}
}

return makeActionBundleFromConfig(config, artifacts, {})
}
4 changes: 4 additions & 0 deletions packages/executor/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ExecutorSelectionStrategy {
// TODO: Fill this in once we know the address
SIMPLE_LOCK = '0x0000000000000000000000000000000000000000',
}
13 changes: 13 additions & 0 deletions packages/executor/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ethers } from 'ethers'

/**
* Parses a comma-separated list of addresses into an array of addresses.
*
* @param strategy Comma-separated list of addresses.
* @returns Array of addresses.
*/
export const parseStrategyString = (strategy: string): string[] => {
return strategy.split(',').map((address) => {
return ethers.utils.getAddress(address)
})
}
3 changes: 3 additions & 0 deletions packages/executor/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './compile'
export * from './constants'
export * from './helpers'
30 changes: 22 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,20 @@
chai "^4.3.4"
ethers "^5.6.8"

"@eth-optimism/core-utils@^0.9.3":
version "0.9.3"
resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.9.3.tgz#40c0271f815af68e0a4715e97a045a96462df7b6"
integrity sha512-b3V8qBgM0e85wdp3CNdJ6iSUvjT2k86F9oCAYeCIXQcQ6+EPaetjxP0T6ct6jLVepnJjoPRlW/lvWslKk1UBGg==
dependencies:
"@ethersproject/abstract-provider" "^5.6.1"
"@ethersproject/properties" "^5.6.0"
"@ethersproject/providers" "^5.6.8"
"@ethersproject/transactions" "^5.6.2"
"@ethersproject/web" "^5.6.1"
bufio "^1.0.7"
chai "^4.3.4"
ethers "^5.6.8"

"@ethereumjs/block@^3.5.0", "@ethereumjs/block@^3.6.2", "@ethereumjs/block@^3.6.3":
version "3.6.3"
resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-3.6.3.tgz#d96cbd7af38b92ebb3424223dbf773f5ccd27f84"
Expand Down Expand Up @@ -1216,9 +1230,9 @@
resolved "https://registry.yarnpkg.com/@rari-capital/solmate/-/solmate-7.0.0-alpha.3.tgz#175aa59f88355a90cc0aa74d514e30d3886345b2"
integrity sha512-cW0PouqpoiEDhhkOxkqQKcjG4oMJ1Qb+kY6aiwBlgOjpfnhmXK7lkU5ZkHKC22ypQj6lvSc6CaHmXXaPywPsYg==

"@rari-capital/solmate@git+https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc":
"@rari-capital/solmate@https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc":
version "7.0.0-alpha.3"
resolved "git+https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc"
resolved "https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc"

"@scure/base@~1.1.0":
version "1.1.1"
Expand Down Expand Up @@ -2883,9 +2897,9 @@ dotenv@^16.0.0, dotenv@^16.0.1:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==

"ds-test@git+https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5":
"ds-test@https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5":
version "0.0.0"
resolved "git+https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5"
resolved "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5"

duplexify@^4.1.1:
version "4.1.2"
Expand Down Expand Up @@ -3570,9 +3584,9 @@ evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"

"excessively-safe-call@git+https://github.com/nomad-xyz/ExcessivelySafeCall.git#4fcdfd3593d21381f696c790fa6180b8ef559c1e":
"excessively-safe-call@https://github.com/nomad-xyz/ExcessivelySafeCall.git#4fcdfd3593d21381f696c790fa6180b8ef559c1e":
version "0.0.1-rc.1"
resolved "git+https://github.com/nomad-xyz/ExcessivelySafeCall.git#4fcdfd3593d21381f696c790fa6180b8ef559c1e"
resolved "https://github.com/nomad-xyz/ExcessivelySafeCall.git#4fcdfd3593d21381f696c790fa6180b8ef559c1e"

execa@^5.0.0:
version "5.1.1"
Expand Down Expand Up @@ -3839,9 +3853,9 @@ foreground-child@^2.0.0:
cross-spawn "^7.0.0"
signal-exit "^3.0.2"

"forge-std@git+https://github.com/foundry-rs/forge-std.git#f18682b2874fc57d7c80a511fed0b35ec4201ffa":
"forge-std@https://github.com/foundry-rs/forge-std.git#f18682b2874fc57d7c80a511fed0b35ec4201ffa":
version "0.0.0"
resolved "git+https://github.com/foundry-rs/forge-std.git#f18682b2874fc57d7c80a511fed0b35ec4201ffa"
resolved "https://github.com/foundry-rs/forge-std.git#f18682b2874fc57d7c80a511fed0b35ec4201ffa"

form-data@^4.0.0:
version "4.0.0"
Expand Down

0 comments on commit 4c73fc1

Please sign in to comment.