Commit 982cb980 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat: introduce the drippie-mon service (#2687)

* feat(ctp): minor Drippie client-side tweaks

Tweaks Drippie to make the client-side a bit simpler. Adds a separate
function "executable" that can be queried to see if a given drip is
currently executable. Updates events so that the name will be properly
emitted but can still be indexed.

* feat: introduce the drippie-mon service

Introduces drippie-mon, a basic service for monitoring Drippie
deployments. drippie-mon will increment a metric whenever a drip is
currently executable. Clients of drippie-mon can watch for this metric
to see if a drip has been executable for a while but has not yet been
executed.
parent c258acd4
---
'@eth-optimism/contracts-periphery': patch
---
Tweaks Drippie contract for client-side ease
---
'@eth-optimism/drippie-mon': minor
---
Release drippie-mon
...@@ -91,6 +91,7 @@ jobs: ...@@ -91,6 +91,7 @@ jobs:
- packages/contracts-periphery/node_modules - packages/contracts-periphery/node_modules
- packages/core-utils/node_modules - packages/core-utils/node_modules
- packages/data-transport-layer/node_modules - packages/data-transport-layer/node_modules
- packages/drippie-mon/node_modules
- packages/fault-detector/node_modules - packages/fault-detector/node_modules
- packages/message-relayer/node_modules - packages/message-relayer/node_modules
- packages/replica-healthcheck/node_modules - packages/replica-healthcheck/node_modules
...@@ -538,6 +539,11 @@ workflows: ...@@ -538,6 +539,11 @@ workflows:
package_name: fault-detector package_name: fault-detector
requires: requires:
- yarn-monorepo - yarn-monorepo
- js-lint-test:
name: drippie-mon-tests
package_name: drippie-mon
requires:
- yarn-monorepo
- js-lint-test: - js-lint-test:
name: message-relayer-tests name: message-relayer-tests
package_name: message-relayer package_name: message-relayer
...@@ -628,6 +634,14 @@ workflows: ...@@ -628,6 +634,14 @@ workflows:
target: fault-detector target: fault-detector
context: context:
- optimism - optimism
- docker-publish:
name: drippie-mon-release
docker_file: ops/docker/Dockerfile.packages
docker_tags: ethereumoptimism/drippie-mon:nightly
docker_context: .
target: drippie-mon
context:
- optimism
- docker-publish: - docker-publish:
name: message-relayer-release name: message-relayer-release
docker_file: ops/docker/Dockerfile.packages docker_file: ops/docker/Dockerfile.packages
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- 'packages/contracts/**/*' - 'packages/contracts/**/*'
- 'packages/contracts-periphery/**/*' - 'packages/contracts-periphery/**/*'
- 'packages/data-transport-layer/**/*' - 'packages/data-transport-layer/**/*'
- 'packages/drippie-mon/**/*'
- 'packages/message-relayer/**/*' - 'packages/message-relayer/**/*'
- 'packages/fault-detector/**/*' - 'packages/fault-detector/**/*'
- 'patches/**/*' - 'patches/**/*'
......
...@@ -18,6 +18,7 @@ jobs: ...@@ -18,6 +18,7 @@ jobs:
l2geth: ${{ steps.packages.outputs.l2geth }} l2geth: ${{ steps.packages.outputs.l2geth }}
message-relayer: ${{ steps.packages.outputs.message-relayer }} message-relayer: ${{ steps.packages.outputs.message-relayer }}
fault-detector: ${{ steps.packages.outputs.fault-detector }} fault-detector: ${{ steps.packages.outputs.fault-detector }}
drippie-mon: ${{ steps.packages.outputs.drippie-mon }}
data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }} data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }}
contracts: ${{ steps.packages.outputs.contracts }} contracts: ${{ steps.packages.outputs.contracts }}
gas-oracle: ${{ steps.packages.outputs.gas-oracle }} gas-oracle: ${{ steps.packages.outputs.gas-oracle }}
...@@ -229,6 +230,33 @@ jobs: ...@@ -229,6 +230,33 @@ jobs:
push: true push: true
tags: ethereumoptimism/fault-detector:${{ needs.canary-publish.outputs.canary-docker-tag }} tags: ethereumoptimism/fault-detector:${{ needs.canary-publish.outputs.canary-docker-tag }}
drippie-mon:
name: Publish Drippie Monitor Version ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish
if: needs.canary-publish.outputs.drippie-mon != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.packages
target: relayer
push: true
tags: ethereumoptimism/drippie-mon:${{ needs.canary-publish.outputs.canary-docker-tag }}
data-transport-layer: data-transport-layer:
name: Publish Data Transport Layer Version ${{ needs.canary-publish.outputs.canary-docker-tag }} name: Publish Data Transport Layer Version ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish needs: canary-publish
......
...@@ -14,6 +14,7 @@ jobs: ...@@ -14,6 +14,7 @@ jobs:
l2geth: ${{ steps.packages.outputs.l2geth }} l2geth: ${{ steps.packages.outputs.l2geth }}
message-relayer: ${{ steps.packages.outputs.message-relayer }} message-relayer: ${{ steps.packages.outputs.message-relayer }}
fault-detector: ${{ steps.packages.outputs.fault-detector }} fault-detector: ${{ steps.packages.outputs.fault-detector }}
drippie-mon: ${{ steps.packages.outputs.drippie-mon }}
data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }} data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }}
contracts: ${{ steps.packages.outputs.contracts }} contracts: ${{ steps.packages.outputs.contracts }}
gas-oracle: ${{ steps.packages.outputs.gas-oracle }} gas-oracle: ${{ steps.packages.outputs.gas-oracle }}
...@@ -372,6 +373,33 @@ jobs: ...@@ -372,6 +373,33 @@ jobs:
push: true push: true
tags: ethereumoptimism/fault-detector:${{ needs.release.outputs.fault-detector }},ethereumoptimism/fault-detector:latest tags: ethereumoptimism/fault-detector:${{ needs.release.outputs.fault-detector }},ethereumoptimism/fault-detector:latest
drippie-mon:
name: Publish Drippie Monitor Version ${{ needs.release.outputs.drippie-mon }}
needs: release
if: needs.release.outputs.drippie-mon != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.packages
target: drippie-mon
push: true
tags: ethereumoptimism/drippie-mon:${{ needs.release.outputs.drippie-mon }},ethereumoptimism/drippie-mon:latest
data-transport-layer: data-transport-layer:
name: Publish Data Transport Layer Version ${{ needs.release.outputs.data-transport-layer }} name: Publish Data Transport Layer Version ${{ needs.release.outputs.data-transport-layer }}
needs: release needs: release
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
{"directory": "packages/contracts", "changeProcessCWD": true }, {"directory": "packages/contracts", "changeProcessCWD": true },
{"directory": "packages/contracts-periphery", "changeProcessCWD": true }, {"directory": "packages/contracts-periphery", "changeProcessCWD": true },
{"directory": "packages/data-transport-layer", "changeProcessCWD": true }, {"directory": "packages/data-transport-layer", "changeProcessCWD": true },
{"directory": "packages/drippie-mon", "changeProcessCWD": true },
{"directory": "packages/batch-submitter", "changeProcessCWD": true }, {"directory": "packages/batch-submitter", "changeProcessCWD": true },
{"directory": "packages/message-relayer", "changeProcessCWD": true }, {"directory": "packages/message-relayer", "changeProcessCWD": true },
{"directory": "packages/fault-detector", "changeProcessCWD": true }, {"directory": "packages/fault-detector", "changeProcessCWD": true },
......
...@@ -36,7 +36,8 @@ root ...@@ -36,7 +36,8 @@ root
│ ├── <a href="./packages/contracts-periphery">contracts-periphery</a>: Peripheral contracts for Optimism │ ├── <a href="./packages/contracts-periphery">contracts-periphery</a>: Peripheral contracts for Optimism
│ ├── <a href="./packages/core-utils">core-utils</a>: Low-level utilities that make building Optimism easier │ ├── <a href="./packages/core-utils">core-utils</a>: Low-level utilities that make building Optimism easier
│ ├── <a href="./packages/data-transport-layer">data-transport-layer</a>: Service for indexing Optimism-related L1 data │ ├── <a href="./packages/data-transport-layer">data-transport-layer</a>: Service for indexing Optimism-related L1 data
│ ├── <a href="./packages/fault-detector">fault-detector</a>: │ ├── <a href="./packages/drippie-mon">drippie-mon</a>: Service for monitoring Drippie instances
│ ├── <a href="./packages/fault-detector">fault-detector</a>: Service for detecting Sequencer faults
│ ├── <a href="./packages/integration-tests-bedrock">integration-tests-bedrock</a> (BEDROCK upgrade): Bedrock integration tests. │ ├── <a href="./packages/integration-tests-bedrock">integration-tests-bedrock</a> (BEDROCK upgrade): Bedrock integration tests.
│ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development │ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development
│ ├── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node │ ├── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node
......
...@@ -52,22 +52,39 @@ contract Drippie is AssetReceiver { ...@@ -52,22 +52,39 @@ contract Drippie is AssetReceiver {
DripStatus status; DripStatus status;
DripConfig config; DripConfig config;
uint256 last; uint256 last;
uint256 count;
} }
/** /**
* Emitted when a new drip is created. * Emitted when a new drip is created.
*/ */
event DripCreated(string indexed name, DripConfig config); event DripCreated(
// Emit name twice because indexed version is hashed.
string indexed nameref,
string name,
DripConfig config
);
/** /**
* Emitted when a drip status is updated. * Emitted when a drip status is updated.
*/ */
event DripStatusUpdated(string indexed name, DripStatus status); event DripStatusUpdated(
// Emit name twice because indexed version is hashed.
string indexed nameref,
string name,
DripStatus status
);
/** /**
* Emitted when a drip is executed. * Emitted when a drip is executed.
*/ */
event DripExecuted(string indexed name, address indexed executor, uint256 timestamp); event DripExecuted(
// Emit name twice because indexed version is hashed.
string indexed nameref,
string name,
address executor,
uint256 timestamp
);
/** /**
* Maps from drip names to drip states. * Maps from drip names to drip states.
...@@ -109,7 +126,7 @@ contract Drippie is AssetReceiver { ...@@ -109,7 +126,7 @@ contract Drippie is AssetReceiver {
} }
// Tell the world! // Tell the world!
emit DripCreated(_name, _config); emit DripCreated(_name, _name, _config);
} }
/** /**
...@@ -163,20 +180,16 @@ contract Drippie is AssetReceiver { ...@@ -163,20 +180,16 @@ contract Drippie is AssetReceiver {
// If we made it here then we can safely update the status. // If we made it here then we can safely update the status.
drips[_name].status = _status; drips[_name].status = _status;
emit DripStatusUpdated(_name, drips[_name].status); emit DripStatusUpdated(_name, _name, drips[_name].status);
} }
/** /**
* Triggers a drip. This function is deliberately left as a public function because the * Checks if a given drip is executable.
* assumption being made here is that setting the drip to ACTIVE is an affirmative signal that
* the drip should be executable according to the drip parameters, drip check, and drip
* interval. Note that drip parameters are read entirely from the state and are not supplied as
* user input, so there should not be any way for a non-authorized user to influence the
* behavior of the drip.
* *
* @param _name Name of the drip to trigger. * @param _name Drip to check.
* @return True if the drip is executable, false otherwise.
*/ */
function drip(string memory _name) external { function executable(string memory _name) public view returns (bool) {
DripState storage state = drips[_name]; DripState storage state = drips[_name];
// Only allow active drips to be executed, an obvious security measure. // Only allow active drips to be executed, an obvious security measure.
...@@ -201,6 +214,29 @@ contract Drippie is AssetReceiver { ...@@ -201,6 +214,29 @@ contract Drippie is AssetReceiver {
"Drippie: dripcheck failed so drip is not yet ready to be triggered" "Drippie: dripcheck failed so drip is not yet ready to be triggered"
); );
// Alright, we're good to execute.
return true;
}
/**
* Triggers a drip. This function is deliberately left as a public function because the
* assumption being made here is that setting the drip to ACTIVE is an affirmative signal that
* the drip should be executable according to the drip parameters, drip check, and drip
* interval. Note that drip parameters are read entirely from the state and are not supplied as
* user input, so there should not be any way for a non-authorized user to influence the
* behavior of the drip.
*
* @param _name Name of the drip to trigger.
*/
function drip(string memory _name) external {
DripState storage state = drips[_name];
// Make sure the drip can be executed.
require(
executable(_name) == true,
"Drippie: drip cannot be executed at this time, try again later"
);
// Update the last execution time for this drip before the call. Note that it's entirely // Update the last execution time for this drip before the call. Note that it's entirely
// possible for a drip to be executed multiple times per block or even multiple times // possible for a drip to be executed multiple times per block or even multiple times
// within the same transaction (via re-entrancy) if the drip interval is set to zero. Users // within the same transaction (via re-entrancy) if the drip interval is set to zero. Users
...@@ -240,6 +276,7 @@ contract Drippie is AssetReceiver { ...@@ -240,6 +276,7 @@ contract Drippie is AssetReceiver {
); );
} }
emit DripExecuted(_name, msg.sender, block.timestamp); state.count++;
emit DripExecuted(_name, _name, msg.sender, block.timestamp);
} }
} }
...@@ -27,7 +27,7 @@ const config: HardhatUserConfig = { ...@@ -27,7 +27,7 @@ const config: HardhatUserConfig = {
}, },
}, },
}, },
opkovan: { 'optimism-kovan': {
chainId: 69, chainId: 69,
url: 'https://kovan.optimism.io', url: 'https://kovan.optimism.io',
verify: { verify: {
...@@ -97,7 +97,10 @@ const config: HardhatUserConfig = { ...@@ -97,7 +97,10 @@ const config: HardhatUserConfig = {
}, },
}, },
namedAccounts: { namedAccounts: {
deployer: `ledger://${getenv('LEDGER_ADDRESS')}`, deployer: {
default: `ledger://${getenv('LEDGER_ADDRESS')}`,
hardhat: 0,
},
}, },
} }
......
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier",
"chai"
]
# RPC pointing to network where Drippie is deployed
DRIPPIE_MON__RPC=
# Address of the Drippie contract
DRIPPIE_MON__DRIPPIE_ADDRESS=
module.exports = {
extends: '../../.eslintrc.js',
}
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
(The MIT License)
Copyright 2020-2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @eth-optimism/drippie-mon
`drippie-mon` is a simple service for monitoring Drippie contracts.
## Installation
Clone, install, and build the Optimism monorepo:
```
git clone https://github.com/ethereum-optimism/optimism.git
yarn install
yarn build
```
## Running the service
Copy `.env.example` into a new file named `.env`, then set the environment variables listed there.
Once your environment variables have been set, run via:
```
yarn start
```
{
"private": true,
"name": "@eth-optimism/drippie-mon",
"version": "0.1.0",
"description": "[Optimism] Service for monitoring Drippie instances",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*"
],
"scripts": {
"start": "ts-node ./src/service.ts",
"test:coverage": "echo 'No tests defined.'",
"build": "tsc -p ./tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint . --max-warnings=0"
},
"keywords": [
"optimism",
"ethereum",
"drippie",
"monitoring"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/drippie-mon#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.9",
"@eth-optimism/contracts-periphery": "0.1.1",
"@eth-optimism/core-utils": "0.8.6",
"@eth-optimism/sdk": "1.1.7",
"ethers": "^5.6.8"
},
"devDependencies": {
"@ethersproject/abstract-provider": "^5.6.1",
"ts-node": "^10.0.0"
}
}
export * from './service'
import {
BaseServiceV2,
Gauge,
Counter,
validators,
} from '@eth-optimism/common-ts'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import * as DrippieArtifact from '@eth-optimism/contracts-periphery/artifacts/contracts/universal/drippie/Drippie.sol/Drippie.json'
type DrippieMonOptions = {
rpc: Provider
drippieAddress: string
}
type DrippieMonMetrics = {
metadata: Gauge
isExecutable: Gauge
executedDripCount: Gauge
unexpectedRpcErrors: Counter
}
type DrippieMonState = {
drippie: ethers.Contract
}
export class DrippieMonService extends BaseServiceV2<
DrippieMonOptions,
DrippieMonMetrics,
DrippieMonState
> {
constructor(options?: Partial<DrippieMonOptions>) {
super({
name: 'drippie-mon',
loop: true,
loopIntervalMs: 60_000,
options,
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network where Drippie is deployed',
},
drippieAddress: {
validator: validators.str,
desc: 'Address of Drippie contract',
},
},
metricsSpec: {
metadata: {
type: Gauge,
desc: 'Drippie Monitor metadata',
labels: ['version', 'address'],
},
isExecutable: {
type: Gauge,
desc: 'Whether or not the drip is currently executable',
labels: ['name'],
},
executedDripCount: {
type: Gauge,
desc: 'Number of times a drip has been executed',
labels: ['name'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
this.state.drippie = new ethers.Contract(
this.options.drippieAddress,
DrippieArtifact.abi,
this.options.rpc
)
this.metrics.metadata.set(
{
// eslint-disable-next-line @typescript-eslint/no-var-requires
version: require('../package.json').version,
address: this.options.drippieAddress,
},
1
)
}
protected async main(): Promise<void> {
let dripCreatedEvents: ethers.Event[]
try {
dripCreatedEvents = await this.state.drippie.queryFilter(
this.state.drippie.filters.DripCreated()
)
} catch (err) {
this.logger.info(`got unexpected RPC error`, {
section: 'creations',
name: 'NULL',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'creations',
name: 'NULL',
})
return
}
// Not the most efficient thing in the world. Will end up making one request for every drip
// created. We don't expect there to be many drips, so this is fine for now. We can also cache
// and skip any archived drips to cut down on a few requests. Worth keeping an eye on this to
// see if it's a bottleneck.
for (const event of dripCreatedEvents) {
const name = event.args.name
let drip: any
try {
drip = await this.state.drippie.drips(name)
} catch (err) {
this.logger.info(`got unexpected RPC error`, {
section: 'drips',
name,
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'drips',
name,
})
continue
}
this.logger.info(`getting drip executable status`, {
name,
count: drip.count.toNumber(),
})
this.metrics.executedDripCount.set(
{
name,
},
drip.count.toNumber()
)
let executable: boolean
try {
// To avoid making unnecessary RPC requests, filter out any drips that we don't expect to
// be executable right now. Only active drips (status = 1) and drips that are due to be
// executed are expected to be executable (but might not be based on the dripcheck).
if (
drip.status === 1 &&
drip.last.toNumber() + drip.config.interval.toNumber() <
Date.now() / 1000
) {
executable = await this.state.drippie.executable(name)
} else {
executable = false
}
} catch (err) {
// All reverts include the string "Drippie:", so we can check for that.
if (err.message.includes('Drippie:')) {
// Not executable yet.
executable = false
} else {
this.logger.info(`got unexpected RPC error`, {
section: 'executable',
name,
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'executable',
name,
})
continue
}
}
this.logger.info(`got drip executable status`, {
name,
executable,
})
this.metrics.isExecutable.set(
{
name,
},
executable ? 1 : 0
)
}
}
}
if (require.main === module) {
const service = new DrippieMonService()
service.run()
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment