Skip to content

Commit

Permalink
refactor(devnet): clarify init script and always mount it
Browse files Browse the repository at this point in the history
  • Loading branch information
egasimus committed Oct 12, 2023
1 parent 4a86789 commit 2983fb2
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 97 deletions.
185 changes: 112 additions & 73 deletions devnets/devnet.init.mjs
Original file line number Diff line number Diff line change
@@ -1,96 +1,135 @@
import { spawn, exec, execSync } from 'child_process'
import { existsSync, writeFileSync, chmodSync } from 'fs'
import { env } from 'node:process'
import { spawn, exec, execSync } from 'node:child_process'
import { existsSync, writeFileSync, chmodSync } from 'node:fs'

const run = command => {
if (process.env.Verbose) console.info('$', command)
const result = String(execSync(command)).trim()
if (process.env.Verbose) console.info(result)
return result
}
const {
VERBOSE = false,

CHAIN_ID = `local-${DAEMON}`,
TOKEN = 'uscrt',
ACCOUNTS = 'Admin Alice Bob Charlie Mallory',
AMOUNT = `1000000000000000000${TOKEN}`,

LCP_PORT = '1317',
RPC_ADDR = 'tcp://0.0.0.0:26657',
GRPC_ADDR = '0.0.0.0:9090',
GRPC_WEB_ADDR = '0.0.0.0:9091',

DAEMON = 'secretd',
STATE_DIR = `/state/${CHAIN_ID}`,
STATE_UID = null,
STATE_GID = null,
} = env

const daemonDir = `~/.${DAEMON}`
const configDir = `${daemonDir}/config`
const appToml = `${configDir}/app.toml`
const genesis = `${configDir}/genesis.json`
const nodeKey = `${configDir}/node_key.json`
const stateDir = `/state/${CHAIN_ID}`
const wallets = `${stateDir}/wallet`

start()

function start ({
lcpPort = process.env.lcpPort || '1317',
rpcAddr = process.env.rpcAddr || 'tcp://0.0.0.0:26657',
grpcAddr = process.env.grpcAddr || '0.0.0.0:9090',
grpcWebAddr = process.env.grpcWebAddr || '0.0.0.0:9091',
genesisJSON = '~/.secretd/config/genesis.json',
} = {}) {
if (!existsSync(genesisJSON)) {
console.info(`${genesisJSON} missing -> performing genesis`)
genesis()
} else {
console.info(`${genesisJSON} exists -> resuming devnet`)
}
function start () {
performGenesis()
spawnLcp()
launchNode()
console.info('Server exited.')
}

function spawnLcp () {
console.info('Configuring the node to support lcp (CORS proxy)...')
run(`perl -i -pe 's;address = "tcp://0.0.0.0:1317";address = "tcp://0.0.0.0:1316";' .secretd/config/app.toml`)
run(`perl -i -pe 's/enable-unsafe-cors = false/enable-unsafe-cors = true/' .secretd/config/app.toml`)
const lcpArgs = [`--proxyUrl`, 'http://localhost:1316', `--port`, lcpPort, `--proxyPartial`, ``]

console.info('Spawning lcp (CORS proxy)...')
if (process.env.Verbose) console.log(`$ lcp`, ...lcpArgs)
run(`perl -i -pe 's;address = "tcp://0.0.0.0:1317";address = "tcp://0.0.0.0:1316";' ${appToml}`)
run(`perl -i -pe 's/enable-unsafe-cors = false/enable-unsafe-cors = true/' ${appToml}`)
const lcpArgs = [
`--proxyUrl`, 'http://localhost:1316',
`--port`, LCP_PORT,
`--proxyPartial`, ``
]
console.info(`Spawning lcp (CORS proxy) on port ${LCP_PORT}`)
if (VERBOSE) console.log(`$ lcp`, ...lcpArgs)
const lcp = spawn(`lcp`, lcpArgs, { stdio: 'inherit' })
}

function launchNode () {
console.info('Launching the node...')
const command = `source /opt/sgxsdk/environment && RUST_BACKTRACE=1 secretd start --bootstrap`
+ ` --rpc.laddr ${rpcAddr}`
+ ` --grpc.address ${grpcAddr}`
+ ` --grpc-web.address ${grpcWebAddr}`
const command = `source /opt/sgxsdk/environment && RUST_BACKTRACE=1 ${DAEMON} start --bootstrap`
+ ` --rpc.laddr ${RPC_ADDR}`
+ ` --grpc.address ${GRPC_ADDR}`
+ ` --grpc-web.address ${GRPC_WEB_ADDR}`
console.info(`$`, command)
execSync(command, { shell: '/bin/bash', stdio: 'inherit' })
console.info('Server exited.')
}

function genesis ({
chainId = process.env.ChainId || 'fadroma-devnet',
stateDir = `/state/${chainId}`,
genesisAccounts = (process.env.GenesisAccounts || 'Admin Alice Bob Charlie Mallory').split(' '),
amount = "1000000000000000000uscrt",
uid = process.env._UID,
gid = process.env._GID
} = {}) {
function performGenesis () {
if (existsSync(genesis)) {
console.info(`Resuming devnet (${genesis} exists).`)
return
}
console.info(`Performing genesis because ${genesis} is missing.`)
preGenesisCleanup()
preGenesisConfig()
createGenesisTransaction()
bootstrapChain()
console.info('\nSprinkling holy water...')
console.info()
}

function preGenesisCleanup () {
console.info('\nEnsuring a clean slate...')
run(`rm -rf ~/.secretd ~/.secretcli /opt/secret/.sgx-secrets`)
run(`rm -rf ${daemonDir} ~/.secretcli /opt/secret/.sgx-secrets`)
}

function preGenesisConfig () {
console.info('\nEstablishing initial config...')
run(`mkdir -p ${stateDir}/wallet`)
if (uid) run(`chown -R ${uid} ${stateDir}`)
if (gid) run(`chgrp -R ${gid} ${stateDir}`)
run(`secretd config chain-id "${chainId}"`)
run(`secretd config keyring-backend test`)
run(`secretd init fadroma-devnet --chain-id "${chainId}"`)
run(`cp ~/node_key.json ~/.secretd/config/node_key.json`)
run(`perl -i -pe 's/"stake"/ "uscrt"/g' ~/.secretd/config/genesis.json`)

console.info('\nCreating genesis accounts', genesisAccounts)
for (const name of genesisAccounts) {
const mnemonic = run(`secretd keys add "${name}" 2>&1 | tail -n1`)
const address = run(`secretd keys show -a "${name}"`)
const identity = `${stateDir}/wallet/${name}.json`
writeFileSync(identity, JSON.stringify({ address, mnemonic }))
if (uid) run(`chown -R ${uid} ${identity}`)
if (gid) run(`chgrp -R ${gid} ${identity}`)
}
if (uid) run(`chown -R ${uid} ${stateDir}`)
if (gid) run(`chgrp -R ${gid} ${stateDir}`)
run(`mkdir -p ${wallets}`)
fixPermissions()
daemon(`config chain-id "${CHAIN_ID}"`)
daemon(`config keyring-backend test`)
daemon(`init fadroma-devnet --chain-id "${CHAIN_ID}"`)
run(`cp ~/node_key.json ${nodeKey}`)
run(`perl -i -pe 's/"stake"/ "${TOKEN}"/g' ${genesis}`)
}

console.info('\nAdding genesis accounts...')
for (const name of genesisAccounts) {
const address = run(`secretd keys show -a "${name}"`)
run(`secretd add-genesis-account "${address}" "${amount}"`)
function createGenesisTransaction () {
let accounts = ACCOUNTS.split(' ')
if (accounts.length === 0) accounts = ['Admin']
console.info('\nCreating genesis accounts:')
for (const name of accounts) {
const mnemonic = daemon(`keys add "${name}" 2>&1 | tail -n1`)
const address = daemon(`keys show -a "${name}"`)
console.info(`\n- ${AMOUNT} ${address} (${name})`)
daemon(`add-genesis-account "${address}" "${AMOUNT}"`)
const identity = `${wallets}/${name}.json`
writeFileSync(identity, JSON.stringify({ address, mnemonic }))
fixPermissions(identity)
}

fixPermissions()
console.info('\nCreating genesis transaction...')
run(`secretd gentx "${genesisAccounts[0]}" 1000000uscrt --chain-id ${chainId} --keyring-backend test`)
daemon(`gentx "${accounts[0]}" 1000000${TOKEN} --chain-id ${CHAIN_ID} --keyring-backend test`)
}

function bootstrapChain () {
console.info('\nBootstrapping chain...')
run(`secretd collect-gentxs`)
run(`secretd validate-genesis`)
run(`secretd init-bootstrap`)
run(`secretd validate-genesis`)
daemon(`collect-gentxs`)
daemon(`validate-genesis`)
daemon(`init-bootstrap`)
daemon(`validate-genesis`)
}

console.info('\nSprinkling holy water...')
console.info()
function fixPermissions (path = stateDir) {
if (STATE_UID) run(`chown -R ${STATE_UID} ${stateDir}`)
if (STATE_GID) run(`chgrp -R ${STATE_GID} ${stateDir}`)
}

function run (command) {
if (VERBOSE) console.info('$', command)
const result = String(execSync(command)).trim()
if (VERBOSE) console.info(result)
return result
}

function daemon (command) {
return run(`${DAEMON} ${command}`)
}
37 changes: 22 additions & 15 deletions fadroma-devnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { Path } from '@hackbg/file'
import { freePort, Endpoint, waitPort, isPortTaken } from '@hackbg/port'
import * as Dock from '@hackbg/dock'

import { dirname } from 'node:path'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { randomBytes } from 'node:crypto'

Expand Down Expand Up @@ -121,14 +121,15 @@ export class Devnet implements DevnetHandle {
}
}
// Apply the rest of the configuration options
const defaultInit = resolve(dirname(fileURLToPath(import.meta.url)), 'devnets', 'devnet.init.mjs')

Check failure on line 124 in fadroma-devnet.ts

View workflow job for this annotation

GitHub Actions / Create and build a project by compiled checkout

The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.

Check failure on line 124 in fadroma-devnet.ts

View workflow job for this annotation

GitHub Actions / pnpm ci

The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.

Check failure on line 124 in fadroma-devnet.ts

View workflow job for this annotation

GitHub Actions / pnpm ci

The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.
this.initScript = options.initScript! ?? defaultInit
this.keepRunning = options.keepRunning ?? !this.deleteOnExit
this.podman = options.podman ?? false
this.platform = options.platform ?? 'scrt_1.9'
this.verbose = options.verbose ?? false
this.launchTimeout = options.launchTimeout ?? 10
this.dontMountState = options.dontMountState ?? false
this.accounts = options.accounts ?? this.accounts
this.initScript = options.initScript! ?? this.initScript
this.readyPhrase = options.readyPhrase ?? Devnet.readyMessage[this.platform]
this.protocol = options.protocol ?? 'http'
this.host = options.host ?? 'localhost'
Expand Down Expand Up @@ -163,28 +164,34 @@ export class Devnet implements DevnetHandle {

/** Environment variables in the container. */
get spawnEnv () {
// Environment variables in devnet container
const env: Record<string, string> = {
Verbose: this.verbose ? 'yes' : '',
ChainId: this.chainId,
GenesisAccounts: this.accounts.join(' '),
_UID: String((process.getuid!)()),
_GID: String((process.getgid!)()),
CHAIN_ID: this.chainId,
ACCOUNTS: this.accounts.join(' '),
STATE_UID: String((process.getuid!)()),
STATE_GID: String((process.getgid!)()),
}
// Which kind of API to expose at the default container port
switch (this.portMode) {
case 'lcp': env.lcpPort = String(this.port); break
case 'grpcWeb': env.grpcWebAddr = `0.0.0.0:${this.port}`; break
default: throw new DevnetError.PortMode(this.portMode)
if (this.verbose) {
env['VERBOSE'] = 'yes'
}
if (this.portMode === 'lcp') {
env['LCP_PORT'] = String(this.port)
} else if (this.portMode === 'grpcWeb') {
env['GRPC_WEB_ADDR'] = `0.0.0.0:${this.port}`
} else {
throw new DevnetError.PortMode(this.portMode)
}
return env
}

/** Options for the container. */
get spawnOptions () {
const Binds: string[] = []
if (this.initScript) Binds.push(`${this.initScript}:${this.initScriptMount}:ro`)
if (!this.dontMountState) Binds.push(`${$(this.stateDir).path}:/state/${this.chainId}:rw`)
if (this.initScript) {
Binds.push(`${this.initScript}:${this.initScriptMount}:ro`)
}
if (!this.dontMountState) {
Binds.push(`${$(this.stateDir).path}:/state/${this.chainId}:rw`)
}
const NetworkMode = 'bridge'
const PortBindings = {[`${this.port}/tcp`]: [{HostPort: `${this.port}`}]}
const HostConfig = {Binds, NetworkMode, PortBindings}
Expand Down
27 changes: 18 additions & 9 deletions spec/Devnet.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//import './Devnet.spec.ts.md'

import { Devnet } from '@hackbg/fadroma'
import * as assert from 'node:assert'
import { getuid, getgid } from 'node:process'
import $, { TextFile } from '@hackbg/file'
import { Image, Container } from '@hackbg/dock'

import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const initScript = resolve(dirname(fileURLToPath(import.meta.url)), 'devnet', 'devnet.init.mjs')

let devnet: any

;(async () => {
Expand All @@ -20,6 +22,9 @@ let devnet: any

await testDevnetHighLevel()

//@ts-ignore
await import('./Devnet.spec.ts.md')

})()

async function testDevnetChainId () {
Expand Down Expand Up @@ -94,7 +99,7 @@ async function testDevnetStateFile () {
async function testDevnetUrl () {

assert.ok(
devnet = new Devnet(),
devnet = new Devnet({ initScript }),
"can construct with no options"
)

Expand All @@ -112,19 +117,23 @@ async function testDevnetUrl () {

async function testDevnetContainer () {

assert.ok(
devnet = new Devnet({ initScript }),
"can construct with explicitly enabled init script"
)

assert.equal(
devnet.initScriptMount, '/devnet.init.mjs',
"devnet init script mounted at default location"
)

assert.deepEqual(
devnet.spawnEnv, {
Verbose: '',
ChainId: devnet.chainId,
GenesisAccounts: devnet.accounts.join(' '),
_UID: getuid!(),
_GID: getgid!(),
lcpPort: String(devnet.port)
CHAIN_ID: devnet.chainId,
ACCOUNTS: devnet.accounts.join(' '),
STATE_UID: String(getuid!()),
STATE_GID: String(getgid!()),
LCP_PORT: String(devnet.port)
},
"devnet spawn environment"
)
Expand Down

0 comments on commit 2983fb2

Please sign in to comment.