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

Add Docker Configuration for Executor #189

Merged
merged 1 commit into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/spicy-geckos-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@chugsplash/executor': patch
'@chugsplash/plugins': patch
---

Add Docker configuration for executor
3 changes: 3 additions & 0 deletions packages/executor/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.env
8 changes: 8 additions & 0 deletions packages/executor/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CHUGSPLASH_EXECUTOR__PRIVATE_KEY=<private key to deploy with>
CHUGSPLASH_EXECUTOR__NETWORK=<network to monitor, http://host.docker.internal:8545 for local node>
CHUGSPLASH_EXECUTOR__AMPLITUDE_KEY=<amplitude api key for analytics (optional)>
IPFS_PROJECT_ID=<ipfs project id to retrieve config file>
IPFS_API_KEY_SECRET=<ipfs api key to retrieve config file>
OPT_ETHERSCAN_API_KEY=<Optimism Etherscan API key>
ETH_ETHERSCAN_API_KEY=<Ethereum Etherscan API key>
INFURA_API_KEY=<Infura Project API Key>
9 changes: 9 additions & 0 deletions packages/executor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:16.16.0

COPY package.json package.json

RUN npm install

COPY . .

CMD [ "yarn", "start" ]
8 changes: 7 additions & 1 deletion packages/executor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"lint:check": "yarn lint:ts:check",
"lint:ts:fix": "yarn lint:ts:check --fix",
"lint:ts:check": "eslint . --max-warnings=0",
"pre-commit": "lint-staged"
"pre-commit": "lint-staged",
"build-container": "docker build --tag chugsplash-executor .",
"run-container": "docker run --env-file ./.env chugsplash-executor"
},
"homepage": "https://github.com/smartcontracts/chugsplash/tree/develop/packages/executor#readme",
"license": "MIT",
Expand All @@ -40,5 +42,9 @@
"hardhat": "^2.10.0",
"ipfs-http-client": "56.0.3",
"undici": "^5.12.0"
},
"dependencies": {
"@amplitude/node": "^1.10.2",
"ts-node": "^10.9.1"
}
}
130 changes: 92 additions & 38 deletions packages/executor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,33 @@ import {
} from '@chugsplash/contracts'
import { ChugSplashBundleState } from '@chugsplash/core'
import { getChainId } from '@eth-optimism/core-utils'
import * as Amplitude from '@amplitude/node'

import { compileRemoteBundle, verifyChugSplashConfig } from './utils'

type Options = {
network: string
privateKey: string
amplitudeKey: string
}

type Metrics = {}

type State = {
registry: ethers.Contract
wallet: ethers.Wallet
ess: string[]
eps: string[]
lastBlockNumber: number
amplitudeClient: Amplitude.NodeClient
}

// TODO:
// Add logging agent for docker container and connect to a managed sink such as logz.io
// Refactor chugsplash commands to decide whether to use the executor based on the target network

export class ChugSplashExecutor extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options>) {
super({
name: 'executor',
name: 'chugsplash-executor',
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
loop: true,
Expand All @@ -37,89 +44,136 @@ export class ChugSplashExecutor extends BaseServiceV2<Options, Metrics, State> {
network: {
desc: 'network for the chain to run the executor on',
validator: validators.str,
default: 'http://localhost:8545',
},
// key: {
// desc: 'private key to use for signing transactions',
// validator: validators.str,
// },
privateKey: {
desc: 'private key used for deployments',
validator: validators.str,
},
amplitudeKey: {
desc: 'API key to send data to Amplitude',
validator: validators.str,
default: 'disabled',
},
},
metricsSpec: {},
})
}

async init() {
if (this.options.amplitudeKey !== 'disabled') {
this.state.amplitudeClient = Amplitude.init(this.options.amplitudeKey)
}

const reg = CHUGSPLASH_REGISTRY_PROXY_ADDRESS
const provider = ethers.getDefaultProvider(this.options.network)
this.state.registry = new ethers.Contract(
reg,
ChugSplashRegistryABI,
provider
)

this.state.wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider)
this.state.lastBlockNumber = -1
this.state.wallet = new ethers.Wallet(this.options.privateKey, provider)
}

async main() {
// Find all active upgrades that have not yet been started
// Find all active upgrades that have not yet been executed in blocks after the stored hash
const approvalAnnouncementEvents = await this.state.registry.queryFilter(
this.state.registry.filters.EventAnnounced('ChugSplashBundleApproved')
this.state.registry.filters.EventAnnounced('ChugSplashBundleApproved'),
this.state.lastBlockNumber + 1
)

// If none found, return
if (approvalAnnouncementEvents.length === 0) {
this.logger.info('no events found')
return
}

this.logger.info(`${approvalAnnouncementEvents.length} events found`)

// store last block number
this.state.lastBlockNumber = approvalAnnouncementEvents.at(-1).blockNumber

// execute all approved bundles
for (const approvalAnnouncementEvent of approvalAnnouncementEvents) {
// fetch manager for relevant project
const signer = this.state.wallet
const manager = new ethers.Contract(
approvalAnnouncementEvent.args.manager,
ChugSplashManagerABI,
signer
)

// get active bundle id for this project
const activeBundleId = await manager.activeBundleId()
if (activeBundleId === ethers.constants.HashZero) {
console.log('no active bundle')
this.logger.error(`Error: No active bundle id found in manager`)
continue
}

// fetch bundle state
const bundleState: ChugSplashBundleState = await manager.bundles(
activeBundleId
)

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

// get proposal event and compile
const proposalEvents = await manager.queryFilter(
manager.filters.ChugSplashBundleProposed(activeBundleId)
)

if (proposalEvents.length !== 1) {
// TODO: throw an error here or skip
}

const proposalEvent = proposalEvents[0]
const { bundle, canonicalConfig } = await compileRemoteBundle(
hre,
proposalEvent.args.configUri
)

// ensure compiled bundle matches proposed bundle
if (bundle.root !== proposalEvent.args.bundleRoot) {
// TODO: throw an error here or skip
// log error and continue
this.logger.error(
'Error: Compiled bundle root does not match proposal event bundle root',
canonicalConfig.options
)
continue
}

// execute bundle
try {
await hre.run('chugsplash-execute', {
chugSplashManager: manager,
bundleState,
bundle,
parsedConfig: canonicalConfig,
deployer: signer,
hide: false,
})
this.logger.info('Successfully executed')
} catch (e) {
// log error and continue
this.logger.error('Error: execution error', e, canonicalConfig.options)
continue
}

// verify on etherscan
try {
if ((await getChainId(this.state.wallet.provider)) !== 31337) {
await verifyChugSplashConfig(hre, proposalEvent.args.configUri)
this.logger.info('Successfully verified')
}
} catch (e) {
this.logger.error(
'Error: verification error',
e,
canonicalConfig.options
)
}

// todo call chugsplash-execute if deploying locally
await hre.run('chugsplash-execute', {
chugSplashManager: manager,
bundleState,
bundle,
parsedConfig: canonicalConfig,
deployer: signer,
hide: false,
})

if ((await getChainId(this.state.wallet.provider)) !== 31337) {
await verifyChugSplashConfig(hre, proposalEvent.args.configUri)
if (this.options.amplitudeKey !== 'disabled') {
this.state.amplitudeClient.logEvent({
event_type: 'ChugSplash Executed',
user_id: canonicalConfig.options.projectOwner,
event_properties: {
projectName: canonicalConfig.options.projectName,
},
})
}
}
}
Expand Down
22 changes: 19 additions & 3 deletions packages/executor/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
{
"extends": "../../tsconfig.json",

"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"sourceMap": true,
"esModuleInterop": true,
"composite": true,
"resolveJsonModule": true,
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"typeRoots": [
"node_modules/@types"
],
"rootDir": "./src",
"outDir": "./dist"
},

"exclude": [
"node_modules",
"dist"
],
sam-goldman marked this conversation as resolved.
Show resolved Hide resolved
"include": [
"src/**/*"
]
Expand Down
3 changes: 2 additions & 1 deletion packages/plugins/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist/
.deployed
artifacts
cache
deployments
deployments/
deployed/