-
Notifications
You must be signed in to change notification settings - Fork 170
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🗞️ Add docker compose spec generation utilities for localnet [4/N] (#478
- Loading branch information
1 parent
9b2ef9f
commit 622fd00
Showing
20 changed files
with
411 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@layerzerolabs/test-devtools": patch | ||
--- | ||
|
||
Add mnemonic arbitrary to test-devtools |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@layerzerolabs/test-devtools": patch | ||
--- | ||
|
||
Add optionalArbitrary |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@layerzerolabs/devtools-evm-hardhat": patch | ||
--- | ||
|
||
Add docker compose utilities for simulation |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// This is a quick, up-to-date version of https://www.npmjs.com/package/jest-raw-loader | ||
// | ||
// We need this in order to be able to statically import/embed simulation Dockerfile & nginx.conf | ||
module.exports = { | ||
process: (content) => ({ code: `module.exports = ${JSON.stringify(content)}` }), | ||
}; |
51 changes: 51 additions & 0 deletions
51
packages/devtools-evm-hardhat/src/simulation/assets/Dockerfile.conf
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
ARG FOUNDRY_VERSION=nightly-156cb1396b7076c6f9cb56f3719f8c90f7f52064 | ||
ARG ALPINE_VERSION=3.18 | ||
|
||
# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- | ||
# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ | ||
# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' | ||
# | ||
# Image that gives us the foundry tools | ||
# | ||
# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- | ||
# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ | ||
# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' | ||
FROM ghcr.io/foundry-rs/foundry:$FOUNDRY_VERSION AS foundry | ||
|
||
# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- | ||
# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ | ||
# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' | ||
# | ||
# Image that starts an EVM node | ||
# | ||
# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- | ||
# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ | ||
# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' | ||
FROM alpine:$ALPINE_VERSION AS node-evm | ||
|
||
STOPSIGNAL SIGINT | ||
|
||
# We will provide a default healthcheck (that assumes that the netowrk is running on the default port 8545) | ||
HEALTHCHECK --timeout=2s --interval=2s --retries=20 CMD cast block --rpc-url http://localhost:8545/ latest | ||
|
||
# Get anvil | ||
COPY --from=foundry /usr/local/bin/anvil /usr/local/bin/anvil | ||
|
||
# Get cast for healthcheck | ||
COPY --from=foundry /usr/local/bin/cast /usr/local/bin/cast | ||
|
||
# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- | ||
# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ | ||
# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' | ||
# | ||
# Image that starts an nginx proxy server | ||
# | ||
# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- | ||
# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ | ||
# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' | ||
FROM nginx:alpine$ALPINE_VERSION AS proxy-evm | ||
|
||
COPY ./nginx.conf /etc/nginx/nginx.conf | ||
|
||
HEALTHCHECK --timeout=2s --interval=2s --retries=20 CMD curl -f http://0.0.0.0:8545/health-check | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// Even though it would be nice to just call this file Dockerfile instead of Dockerfile.conf, | ||
// esbuild (or tsup) have issues with specifying loaders for files without extensions. | ||
// | ||
// And since we already have a file with .conf extension, | ||
// we add the same extension to the Dockerfile to use the same d.ts file and the same loader | ||
export { default as dockerfile } from './Dockerfile.conf' | ||
export { default as nginxConf } from './nginx.conf' |
56 changes: 56 additions & 0 deletions
56
packages/devtools-evm-hardhat/src/simulation/assets/nginx.conf
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
events {} | ||
|
||
http { | ||
# We will modify the log format to include the target_network | ||
log_format proxied '$remote_addr - $remote_user [$time_local] ' | ||
'"$request" $status $body_bytes_sent ' | ||
'"$http_referer" "$http_user_agent" ' | ||
'Network: "$target_network"'; | ||
|
||
server { | ||
# This proxy server will listen on port 8545 | ||
# | ||
# Even though it's not ideal to have this hardcoded, this port | ||
# will be remapped to a desired host port using docker compose, | ||
# the only issue this hardcoding brings is the fact that this port | ||
# needs to match the container port in the compose spec | ||
listen 8545; | ||
listen [::]:8545; | ||
|
||
# We will add a simple endpoint for healthcheck | ||
location /health-check { | ||
access_log off; | ||
error_log off; | ||
return 200 'ok'; | ||
} | ||
|
||
# In this section we'll proxy all the requests to this server | ||
# to the respective network nodes | ||
# | ||
# The requests are proxied based on the first path segment: | ||
# | ||
# http://localhost/fuji -> http://fuji:8545/ | ||
# | ||
# For now the remaining path segments are not being preserved: | ||
# | ||
# # http://localhost/fuji/some/url -> http://fuji:8545/ | ||
location / { | ||
# Set the log format to be our custom 'proxied' log format | ||
access_log /var/log/nginx/access.log proxied; | ||
|
||
resolver 127.0.0.11; | ||
autoindex off; | ||
|
||
# This variable will hold the name of the network to proxy to | ||
set $target_network ''; | ||
|
||
# Extract the first path segment from the request URI | ||
if ($request_uri ~* ^/(?<target_network>[^/]+)(/.*)?$) { | ||
set $target_network $1; | ||
} | ||
|
||
# Proxy the request to the appropriate network | ||
proxy_pass http://$target_network:8545/; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { pipe } from 'fp-ts/lib/function' | ||
import * as RR from 'fp-ts/ReadonlyRecord' | ||
import type { ComposeSpec, ComposeSpecService } from '@layerzerolabs/devtools' | ||
import { type AnvilOptions, createAnvilCliOptions } from '@layerzerolabs/devtools-evm' | ||
import type { ComposeSpecServices } from '@layerzerolabs/devtools' | ||
import type { SimulationConfig } from './types' | ||
|
||
/** | ||
* Creates a docker compose service specification for an anvil-based EVM node | ||
* | ||
* @param {AnvilOptions} anvilOptions | ||
* @returns {ComposeSpecService} | ||
*/ | ||
export const createEvmNodeServiceSpec = (anvilOptions: AnvilOptions): ComposeSpecService => ({ | ||
// This service references a Dockerfile that is copied | ||
// next to the resulting docker-compose.yaml | ||
// | ||
// The source for this Dockerfile is located in src/simulation/assets/Dockerfile.conf | ||
build: { | ||
dockerfile: 'Dockerfile', | ||
target: 'node-evm', | ||
}, | ||
command: ['anvil', ...createAnvilCliOptions(anvilOptions)], | ||
}) | ||
|
||
/** | ||
* Creates a docker compose service specification for an nginx-based proxy service | ||
* that proxies requests to underlying EVM nodes (or their RPC URLs to be mor precise) | ||
* | ||
* @param {number} port | ||
* @param {ComposeSpecServices} networkServices | ||
* @returns {ComposeSpecService} | ||
*/ | ||
export const createEvmNodeProxyServiceSpec = ( | ||
port: number, | ||
networkServices: ComposeSpecServices | ||
): ComposeSpecService => ({ | ||
// This service references a Dockerfile that is copied | ||
// next to the resulting docker-compose.yaml | ||
// | ||
// The source for this Dockerfile is located in src/simulation/assets/Dockerfile.conf | ||
build: { | ||
dockerfile: 'Dockerfile', | ||
target: 'proxy-evm', | ||
}, | ||
// This service will expose its internal 8545 port to a host port | ||
// | ||
// The internal 8545 port is hardcoded both here and in the nginx.conf file, | ||
// the source for which is located in src/simulation/assets/nginx.conf | ||
ports: [`${port}:8545`], | ||
depends_on: pipe( | ||
networkServices, | ||
// This service will depend on the RPCs to be healthy | ||
// so we'll take the networkServices object and replace | ||
// the values with service_healthy condition | ||
RR.map(() => ({ | ||
condition: 'service_healthy', | ||
})) | ||
), | ||
}) | ||
|
||
/** | ||
* Creates a docker compose spec with a set of anvil-based EVM nodes | ||
* and a single proxy server that proxies requests to these nodes. | ||
* | ||
* @param {SimulationConfig} config | ||
* @param {Record<string, AnvilOptions>} networks | ||
* @returns {ComposeSpec} | ||
*/ | ||
export const createSimulationComposeSpec = ( | ||
config: SimulationConfig, | ||
networks: Record<string, AnvilOptions> | ||
): ComposeSpec => ({ | ||
version: '3.9', | ||
services: pipe( | ||
networks, | ||
// First we turn the networks into docker compose specs for EVM nodes | ||
RR.map(createEvmNodeServiceSpec), | ||
(networkServiceSpecs) => | ||
// Then we add the RPC proxy server | ||
// | ||
// There is a small edge case here that we can address | ||
// if it ever comes up: if a network is called 'rpc', this compose file | ||
// will not work. | ||
// | ||
// The fix for this is to prefix all networks with something like network-xxx | ||
// but we can do that if ever this usecase comes up | ||
pipe( | ||
networkServiceSpecs, | ||
RR.upsertAt('rpc', createEvmNodeProxyServiceSpec(config.port, networkServiceSpecs)) | ||
) | ||
), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './compose' | ||
export * from './config' | ||
export * from './types' |
13 changes: 13 additions & 0 deletions
13
packages/devtools-evm-hardhat/test/simulation/__snapshots__/compose.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`simulation/compose createEvmNodeServiceSpec() should work when there are no anvil options 1`] = ` | ||
"version: '3.9' | ||
services: | ||
anvil: | ||
build: | ||
dockerfile: Dockerfile | ||
target: node-evm | ||
command: | ||
- anvil | ||
" | ||
`; |
130 changes: 130 additions & 0 deletions
130
packages/devtools-evm-hardhat/test/simulation/compose.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import hre from 'hardhat' | ||
import { resolveSimulationConfig } from '@/simulation' | ||
import { | ||
createEvmNodeProxyServiceSpec, | ||
createEvmNodeServiceSpec, | ||
createSimulationComposeSpec, | ||
} from '@/simulation/compose' | ||
import { serializeDockerComposeSpec } from '@layerzerolabs/devtools' | ||
import { AnvilOptions } from '@layerzerolabs/devtools-evm' | ||
import { mnemonicArbitrary, optionalArbitrary } from '@layerzerolabs/test-devtools' | ||
import { spawnSync } from 'child_process' | ||
import fc from 'fast-check' | ||
import { rm, writeFile } from 'fs/promises' | ||
import { join } from 'path' | ||
|
||
describe('simulation/compose', () => { | ||
// The available docker port range | ||
const portArbitrary = fc.integer({ min: 1, max: 65535 }) | ||
|
||
const anvilOptionsArbitrary: fc.Arbitrary<AnvilOptions> = fc.record({ | ||
host: optionalArbitrary(fc.ipV4()), | ||
port: optionalArbitrary(portArbitrary), | ||
count: optionalArbitrary(fc.integer()), | ||
mnemonic: optionalArbitrary(mnemonicArbitrary), | ||
blockTime: optionalArbitrary(fc.integer()), | ||
forkUrl: optionalArbitrary(fc.webUrl()), | ||
}) | ||
|
||
// We are aware of shortcomings of the simulation spec generation process: | ||
// | ||
// - whitespace service names will generate invalid compose files. This should not be a problem in a real hardhat project | ||
// - network named rpc will collide with the RPC proxy container defined in the spec | ||
// | ||
// Because of this we'll filter down on the possibilities when it comes to service names | ||
const serviceNameArbitrary = fc.constantFrom('mumbai', 'sepolia-testnet', 'someNetwork', 'someNetwork.V2') | ||
|
||
const SPEC_FILE_PATH = join(__dirname, 'docker-compose.yaml') | ||
|
||
// We add --log-level to suppress any warnings - for example warnings about environment variables not being defined | ||
// | ||
// This allows us to easily test the stderr for being empty (otherwise we would need to test it for not containing errors) | ||
const validateSpec = () => spawnSync('docker', ['--log-level', 'ERROR', 'compose', '-f', SPEC_FILE_PATH, 'config']) | ||
|
||
afterEach(async () => { | ||
await rm(SPEC_FILE_PATH, { force: true }) | ||
}) | ||
|
||
describe('createEvmNodeServiceSpec()', () => { | ||
it('should work when there are no anvil options', async () => { | ||
const spec = serializeDockerComposeSpec({ | ||
version: '3.9', | ||
services: { | ||
anvil: createEvmNodeServiceSpec({}), | ||
}, | ||
}) | ||
|
||
await writeFile(SPEC_FILE_PATH, spec) | ||
|
||
expect(validateSpec().stderr.toString('utf8')).toBe('') | ||
expect(validateSpec().status).toBe(0) | ||
|
||
expect(spec).toMatchSnapshot() | ||
}) | ||
|
||
it('should work with anvil options', async () => { | ||
await fc.assert( | ||
fc.asyncProperty(anvilOptionsArbitrary, async (anvilOptions) => { | ||
const spec = serializeDockerComposeSpec({ | ||
version: '3.9', | ||
services: { | ||
anvil: createEvmNodeServiceSpec(anvilOptions), | ||
}, | ||
}) | ||
|
||
await writeFile(SPEC_FILE_PATH, spec) | ||
|
||
expect(validateSpec().stderr.toString('utf8')).toBe('') | ||
expect(validateSpec().status).toBe(0) | ||
}), | ||
{ numRuns: 20 } | ||
) | ||
}) | ||
}) | ||
|
||
describe('createEvmNodeProxyServiceSpec()', () => { | ||
const servicesArbitrary = fc.dictionary( | ||
serviceNameArbitrary, | ||
anvilOptionsArbitrary.map(createEvmNodeServiceSpec) | ||
) | ||
|
||
it('should work with anvil services', async () => { | ||
await fc.assert( | ||
fc.asyncProperty(portArbitrary, servicesArbitrary, async (port, services) => { | ||
const spec = serializeDockerComposeSpec({ | ||
version: '3.9', | ||
services: { | ||
...services, | ||
rpc: createEvmNodeProxyServiceSpec(port, services), | ||
}, | ||
}) | ||
|
||
await writeFile(SPEC_FILE_PATH, spec) | ||
|
||
expect(validateSpec().stderr.toString('utf8')).toBe('') | ||
expect(validateSpec().status).toBe(0) | ||
}), | ||
{ numRuns: 20 } | ||
) | ||
}) | ||
}) | ||
|
||
describe('createSimulationComposeSpec()', () => { | ||
const anvilOptionsRecordArbitrary = fc.dictionary(serviceNameArbitrary, anvilOptionsArbitrary) | ||
|
||
it('should work goddammit', async () => { | ||
await fc.assert( | ||
fc.asyncProperty(portArbitrary, anvilOptionsRecordArbitrary, async (port, anvilOptions) => { | ||
const simulationConfig = resolveSimulationConfig({ port }, hre.config) | ||
const spec = serializeDockerComposeSpec(createSimulationComposeSpec(simulationConfig, anvilOptions)) | ||
|
||
await writeFile(SPEC_FILE_PATH, spec) | ||
|
||
expect(validateSpec().stderr.toString('utf8')).toBe('') | ||
expect(validateSpec().status).toBe(0) | ||
}), | ||
{ numRuns: 20 } | ||
) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.