import "../_dnt.polyfills.js"; import * as dntShim from "../_dnt.shims.js"; import { hex } from "../crypto/mod.js" import * as ed25519 from "../deps/ed25519.js" import * as base58 from "../deps/std/encoding/base58.js" import * as path from "../deps/std/path.js" import { writableStreamFromWriter } from "../deps/std/streams.js" import { PermanentMemo } from "../util/memo.js" import { getFreePort, portReady } from "../util/port.node.js" import { BinaryResolver } from "./bins.js" import { createRawChainSpec } from "./chain_spec/mod.js" import type { DevRelaySpec } from "./DevRelaySpec.js" import { getMetadataFromWsUrl, NetProps, NetSpec } from "./NetSpec.js" export interface DevNetProps extends NetProps { bin: string | BinaryResolver chain: string nodeCount?: number customize?: (chainSpec: Record) => void } export abstract class DevNetSpec extends NetSpec { readonly binary readonly chain readonly nodeCount readonly customize constructor(props: DevNetProps) { super(props) const { bin } = props this.binary = typeof bin === "string" ? (() => Promise.resolve(bin)) : bin this.chain = props.chain this.nodeCount = props.nodeCount this.customize = props.customize } abstract relay: DevRelaySpec abstract preflightNetworkArgs(signal: AbortSignal, devnetTempDir: string): Promise connection(name: string) { return { type: "DevnetConnection" as const, discovery: name, } } tempDir(parentDir: string) { return path.join(parentDir, this.name) } readonly #rawChainSpecPaths = new PermanentMemo() async rawChainSpecPath(signal: AbortSignal, devnetTempDir: string) { const tempDir = this.tempDir(devnetTempDir) return this.#rawChainSpecPaths.run( tempDir, async () => createRawChainSpec(tempDir, await this.binary(signal), this.chain), ) } async preflightNetwork(signal: AbortSignal, devnetTempDir: string) { const [chainSpecPath, extraArgs] = await Promise.all([ this.rawChainSpecPath(signal, devnetTempDir), this.preflightNetworkArgs(signal, devnetTempDir), ]) return spawnDevNet({ tempDir: this.tempDir(devnetTempDir), binary: await this.binary(signal), chainSpecPath, nodeCount: 1, extraArgs, signal, }) } async metadata(signal: AbortSignal, devnetTempDir: string) { const { ports: [port0] } = await this.preflightNetwork(signal, devnetTempDir) return getMetadataFromWsUrl(`ws://127.0.0.1:${port0}`) } } export interface SpawnDevNetProps { tempDir: string binary: string chainSpecPath: string nodeCount: number extraArgs: string[] signal: AbortSignal } export interface DevNet { bootnodes: string ports: number[] } const keystoreAccounts = ["alice", "bob", "charlie", "dave", "eve", "ferdie"] export async function spawnDevNet({ tempDir, binary, chainSpecPath, nodeCount, extraArgs, signal, }: SpawnDevNetProps): Promise { let bootnodes: string | undefined const ports = [] for (let i = 0; i < nodeCount; i++) { const keystoreAccount = keystoreAccounts[i] if (!keystoreAccount) throw new Error("ran out of keystore accounts") const nodeDir = path.join(tempDir, keystoreAccount) await dntShim.Deno.mkdir(nodeDir, { recursive: true }) const [httpPort, wsPort, wsPortArgName_] = await Promise.all([ getFreePort(), getFreePort(), wsPortArgName(binary), ]) ports.push(wsPort) const args = [ "--validator", `--${keystoreAccount}`, "--base-path", nodeDir, "--chain", chainSpecPath, "--port", `${httpPort}`, wsPortArgName_, `${wsPort}`, ] if (bootnodes) { args.push("--bootnodes", bootnodes) } else { const nodeKey = crypto.getRandomValues(new Uint8Array(32)) args.push("--node-key", hex.encode(nodeKey)) const publicKey = await ed25519.getPublicKeyAsync(nodeKey) // Peer IDs are derived by hashing the encoded public key with multihash. // See https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#peer-ids // For any 32 byte ed25519 public key the first 6 bytes are always [0, 36, 8, 1, 18, 32] // PeerId = [0, 36, 8, 1, 18, 32, ...publicKey] // -------------------------- > protobuf encoded ed25519 public key (36 bytes) // --------------------------------- > identity multihash of the protobuf encoded ed25519 public key (38 bytes) const peerId = base58.encode(new Uint8Array([0, 36, 8, 1, 18, 32, ...publicKey])) bootnodes = `/ip4/127.0.0.1/tcp/${httpPort}/p2p/${peerId}` } args.push(...extraArgs) spawnNode(nodeDir, binary, args, signal) await portReady(wsPort) } if (!bootnodes) throw new Error("count must be > 1") return { bootnodes, ports } } async function spawnNode(tempDir: string, binary: string, args: string[], signal: AbortSignal) { const child = new dntShim.Deno.Command(binary, { args, signal, stdout: "piped", stderr: "piped", }).spawn() child.stdout.pipeTo( writableStreamFromWriter( await dntShim.Deno.open(path.join(tempDir, "stdout"), { write: true, create: true }), ), ) child.stderr.pipeTo( writableStreamFromWriter( await dntShim.Deno.open(path.join(tempDir, "stderr"), { write: true, create: true }), ), ) child.status.then((status) => { if (!signal.aborted) { throw new Error(`process exited with code ${status.code} (${tempDir})`) } }) } async function wsPortArgName(binary: string): Promise { const output = await new dntShim.Deno.Command(binary, { args: ["--help"] }).output() return new TextDecoder().decode(output.stdout).includes("--ws-port") ? "--ws-port" : "--rpc-port" }