From 6b754dbe657186d254a11bbab9453d42d0cf63fc Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Wed, 23 Oct 2024 13:53:02 +1300 Subject: [PATCH] Cli refactor (#189) * refactor cli handling * script to launch network * update * fix schema * fix test --- .../Config/ProtocolConfig+Preset.swift | 38 ++++ .../xcshareddata/xcschemes/Boka.xcscheme | 2 +- Boka/Package.resolved | 17 +- Boka/Package.swift | 2 + Boka/Sources/Boka.swift | 201 ++++++++---------- Boka/Sources/main.swift | 7 - Boka/Tests/BokaTests/BokaTests.swift | 24 +-- Makefile | 8 +- Node/Package.resolved | 6 +- Node/Sources/Node/Genesis.swift | 48 +++-- .../xcshareddata/swiftpm/Package.resolved | 8 +- scripts/devnet.sh | 32 +++ 12 files changed, 225 insertions(+), 168 deletions(-) delete mode 100644 Boka/Sources/main.swift create mode 100755 scripts/devnet.sh diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift index 91399c7b..58b88109 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift @@ -1,6 +1,44 @@ import Utils extension Ref where T == ProtocolConfig { + // TODO: pick some good numbers for dev env + public static let minimal = Ref(ProtocolConfig( + auditTranchePeriod: 8, + additionalMinBalancePerStateItem: 10, + additionalMinBalancePerStateByte: 1, + serviceMinBalance: 100, + totalNumberOfCores: 1, + preimagePurgePeriod: 28800, + epochLength: 6, + auditBiasFactor: 2, + coreAccumulationGas: Gas(10_000_000), // TODO: check this + workPackageAuthorizerGas: Gas(10_000_000), // TODO: check this + workPackageRefineGas: Gas(10_000_000), // TODO: check this + recentHistorySize: 8, + maxWorkItems: 4, + maxTicketsPerExtrinsic: 4, + maxLookupAnchorAge: 14400, + transferMemoSize: 128, + ticketEntriesPerValidator: 2, + maxAuthorizationsPoolItems: 8, + slotPeriodSeconds: 4, + maxAuthorizationsQueueItems: 10, + coreAssignmentRotationPeriod: 6, + maxServiceCodeSize: 4_000_000, + preimageReplacementPeriod: 5, + totalNumberOfValidators: 3, + erasureCodedPieceSize: 684, + maxWorkPackageManifestEntries: 1 << 11, + maxEncodedWorkPackageSize: 12 * 1 << 20, + maxEncodedWorkReportSize: 96 * 1 << 10, + erasureCodedSegmentSize: 6, + ticketSubmissionEndSlot: 2, + pvmDynamicAddressAlignmentFactor: 2, + pvmProgramInitInputDataSize: 1 << 24, + pvmProgramInitPageSize: 1 << 14, + pvmProgramInitSegmentSize: 1 << 16 + )) + // TODO: pick some good numbers for dev env public static let dev = Ref(ProtocolConfig( auditTranchePeriod: 8, diff --git a/Boka/.swiftpm/xcode/xcshareddata/xcschemes/Boka.xcscheme b/Boka/.swiftpm/xcode/xcshareddata/xcschemes/Boka.xcscheme index 207805d9..1dec0787 100644 --- a/Boka/.swiftpm/xcode/xcshareddata/xcschemes/Boka.xcscheme +++ b/Boka/.swiftpm/xcode/xcshareddata/xcschemes/Boka.xcscheme @@ -64,7 +64,7 @@ diff --git a/Boka/Package.resolved b/Boka/Package.resolved index fbf27596..d527cf1c 100644 --- a/Boka/Package.resolved +++ b/Boka/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "cee187bf5c40292c311294a895cbf00f2bef24f9ce8e07d377d38db947218bc0", + "originHash" : "60a8204ece9450dc69d2c41c6386c74b8d08e6a44261244fb5adeadc19c30e03", "pins" : [ { "identity" : "async-channels", "kind" : "remoteSourceControl", "location" : "https://github.com/gh123man/Async-Channels.git", "state" : { - "revision" : "37d32cfc70f08b72a38a2c40f65338ee023afa45", - "version" : "1.0.1" + "revision" : "e4c71cd0364532d34b27e76f4ab7229abaaaefb4", + "version" : "1.0.2" } }, { @@ -82,6 +82,15 @@ "version" : "1.2.0" } }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, { "identity" : "swift-asn1", "kind" : "remoteSourceControl", @@ -220,7 +229,7 @@ { "identity" : "swift-numerics", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics.git", + "location" : "https://github.com/apple/swift-numerics", "state" : { "branch" : "main", "revision" : "e30276bff2ff5ed80566fbdca49f50aa160b0e83" diff --git a/Boka/Package.swift b/Boka/Package.swift index c6c63b18..a6d59481 100644 --- a/Boka/Package.swift +++ b/Boka/Package.swift @@ -15,6 +15,7 @@ let package = Package( .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.0"), .package(url: "https://github.com/vapor/console-kit.git", from: "4.15.0"), .package(url: "https://github.com/apple/swift-testing.git", branch: "0.10.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -28,6 +29,7 @@ let package = Package( .product(name: "OTel", package: "swift-otel"), .product(name: "OTLPGRPC", package: "swift-otel"), .product(name: "ConsoleKit", package: "console-kit"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), .testTarget( diff --git a/Boka/Sources/Boka.swift b/Boka/Sources/Boka.swift index 6bd55556..041933c7 100644 --- a/Boka/Sources/Boka.swift +++ b/Boka/Sources/Boka.swift @@ -1,74 +1,87 @@ +import ArgumentParser import Blockchain -import ConsoleKit import Foundation import Node import ServiceLifecycle import TracingUtils import Utils -enum InvalidArgumentError: Error { - case invalidArgument(String) +extension Genesis: @retroactive ExpressibleByArgument { + public init?(argument: String) { + if let preset = GenesisPreset(rawValue: argument) { + self = .preset(preset) + } else { + self = .file(path: argument) + } + } } -struct Boka: AsyncCommand { - struct Signature: CommandSignature { - @Option(name: "base-path", short: "d", help: "Base path to database files.") - var basePath: String? +extension NetAddr: @retroactive ExpressibleByArgument { + public init?(argument: String) { + self.init(address: argument) + } +} - @Option(name: "chain", short: "c", help: "Path to chain spec file.") - var chain: String? +enum MaybeEnabled: ExpressibleByArgument { + case enabled(T) + case disabled - @Option(name: "config-file", short: "f", help: "Path to config file.") - var configFile: String? + init?(argument: String) { + if argument.lowercased() == "false" { + self = .disabled + } else { + guard let argument = T(argument: argument) else { + return nil + } + self = .enabled(argument) + } + } - @Option( - name: "rpc", - help: - "Listen address for RPC server. Pass 'false' to disable RPC server. Default to 127.0.0.1:9955." - ) - var rpc: String? + var asOptional: T? { + switch self { + case let .enabled(value): + value + case .disabled: + nil + } + } +} - @Option(name: "p2p", help: "Listen address for P2P protocol.") - var p2p: String? +@main +struct Boka: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "JAM built with Swift", + version: "0.0.1" + ) - @Option(name: "peers", help: "Specify peer P2P addresses separated by commas.") - var peers: String? + @Option(name: .shortAndLong, help: "Base path to database files.") + var basePath: String? - @Flag(name: "validator", help: "Run as a validator.") - var validator: Bool + @Option(name: .long, help: "A preset config or path to chain config file.") + var chain: Genesis = .preset(.dev) - @Option( - name: "operator-rpc", - help: - "Listen address for operator RPC server. Pass 'false' to disable operator RPC server. Default to false." - ) - var operatorRpc: String? + @Option(name: .long, help: "Listen address for RPC server. Pass 'false' to disable RPC server. Default to 127.0.0.1:9955.") + var rpc: MaybeEnabled = .enabled(NetAddr(address: "127.0.0.1:9955")!) - @Option(name: "dev-seed", help: "For development only. Seed for validator keys.") - var devSeed: String? + @Option(name: .long, help: "Listen address for P2P protocol.") + var p2p: NetAddr = .init(address: "127.0.0.1:19955")! - @Flag(name: "version", help: "Show the version.") - var version: Bool + @Option(name: .long, help: "Specify peer P2P addresses.") + var peers: [NetAddr] = [] - @Flag(name: "help", short: "h", help: "Show help information.") - var help: Bool - } + @Flag(name: .long, help: "Run as a validator.") + var validator = false - var help: String { - "A command-line tool for Boka." - } + @Option(name: .long, help: "Listen address for operator RPC server. Pass 'false' to disable operator RPC server. Default to false.") + var operatorRpc: NetAddr? - func run(using context: CommandContext, signature: Signature) async throws { - if signature.help { - context.console.info(help) - return - } - // TODO: fix version number issue #168 - if signature.version { - context.console.info("Boka version 1.0.0") - return - } + @Option(name: .long, help: "For development only. Seed for validator keys.") + var devSeed: UInt32? + + @Option(name: .long, help: "Node name. For telemetry only.") + var name: String? + mutating func run() async throws { let services = try await Tracing.bootstrap("Boka", loggerOnly: true) for service in services { Task { @@ -76,72 +89,50 @@ struct Boka: AsyncCommand { } } - // Handle other options and flags - if let basePath = signature.basePath { - context.console.info("Base path: \(basePath)") - } + let logger = Logger(label: "cli") - if let peers = signature.peers { - let peerList = peers.split(separator: ",").map { - $0.trimmingCharacters(in: .whitespaces) - } - context.console.info("Peers: \(peerList.joined(separator: ", "))") - } + logger.info("Starting Boka. Chain: \(chain)") - if signature.validator { - context.console.info("Running as validator") + if let name { + logger.info("Node name: \(name)") } - if let operatorRpc = signature.operatorRpc { - context.console.info("Operator RPC listen address: \(operatorRpc)") + if let basePath { + logger.info("Base path: \(basePath)") } - var rpcListenAddress = NetAddr(ipAddress: "127.0.0.1", port: 9955) - if let rpc = signature.rpc { - if rpc.lowercased() == "false" { - context.console.info("RPC server is disabled") - rpcListenAddress = nil - } else { - if let addr = NetAddr(address: rpc) { - rpcListenAddress = addr - } else { - throw InvalidArgumentError.invalidArgument("Invalid RPC address") - } - } + logger.info("Peers: \(peers)") + + if validator { + logger.info("Running as validator") + } else { + logger.info("Running as fullnode") } - let rpcConfig = rpcListenAddress.map { rpcListenAddress in - let (rpcAddress, rpcPort) = rpcListenAddress.getAddressAndPort() - return RPCConfig(listenAddress: rpcAddress, port: Int(rpcPort)) + if let operatorRpc { + logger.info("Operator RPC listen address: \(operatorRpc)") } - var p2pListenAddress = NetAddr(ipAddress: "127.0.0.1", port: 19955)! - if let p2p = signature.p2p { - if let addr = NetAddr(address: p2p) { - p2pListenAddress = addr - } else { - throw InvalidArgumentError.invalidArgument("Invalid P2P address") - } + let rpcConfig = rpc.asOptional.map { addr -> RPCConfig in + logger.info("RPC listen address: \(addr)") + let (address, port) = addr.getAddressAndPort() + return RPCConfig(listenAddress: address, port: Int(port)) } let keystore = try await DevKeyStore() - var devKey: KeySet? - let networkKey: Ed25519.SecretKey - if let devSeed = signature.devSeed { - guard let val = UInt32(devSeed) else { - throw InvalidArgumentError.invalidArgument("devSeed is not a valid hex string") + let networkKey: Ed25519.SecretKey = try await { + if let devSeed { + let key = try await keystore.addDevKeys(seed: devSeed) + return await keystore.get(Ed25519.self, publicKey: key.ed25519)! + } else { + return try await keystore.generate(Ed25519.self) } - devKey = try await keystore.addDevKeys(seed: val) - networkKey = await keystore.get(Ed25519.self, publicKey: devKey!.ed25519)! - } else { - // TODO: only generate network key if keystore is empty - networkKey = try await keystore.generate(Ed25519.self) - } + }() let networkConfig = NetworkConfig( - mode: signature.validator ? .validator : .builder, - listenAddress: p2pListenAddress, + mode: validator ? .validator : .builder, + listenAddress: p2p, key: networkKey ) @@ -153,26 +144,20 @@ struct Boka: AsyncCommand { handlerMiddleware: .tracing(prefix: "Handler") ) - var genesis: Genesis = .dev - if let configFile = signature.configFile { - context.console.info("Config file: \(configFile)") - genesis = .file(path: configFile) - } - let config = Node.Config(rpc: rpcConfig, network: networkConfig) - let node: Node = if signature.validator { + let node: Node = if validator { try await ValidatorNode( - config: config, genesis: genesis, eventBus: eventBus, keystore: keystore + config: config, genesis: chain, eventBus: eventBus, keystore: keystore ) } else { try await Node( - config: config, genesis: genesis, eventBus: eventBus, keystore: keystore + config: config, genesis: chain, eventBus: eventBus, keystore: keystore ) } try await node.wait() - console.info("Shutting down...") + logger.notice("Shutting down...") } } diff --git a/Boka/Sources/main.swift b/Boka/Sources/main.swift deleted file mode 100644 index a07cec9d..00000000 --- a/Boka/Sources/main.swift +++ /dev/null @@ -1,7 +0,0 @@ -import ConsoleKit - -// Set up the CLI -let input = CommandInput(arguments: CommandLine.arguments) -let console = Terminal() -let boka = Boka() -try await console.run(boka, input: input) diff --git a/Boka/Tests/BokaTests/BokaTests.swift b/Boka/Tests/BokaTests/BokaTests.swift index 802ed3e2..1226f1f2 100644 --- a/Boka/Tests/BokaTests/BokaTests.swift +++ b/Boka/Tests/BokaTests/BokaTests.swift @@ -1,5 +1,4 @@ import Blockchain -import ConsoleKit import Foundation import Node import Testing @@ -13,27 +12,12 @@ enum ResourceLoader { } } -final class BokaTests { - var console: Terminal - var boka: Boka - init() { - console = Terminal() - boka = Boka() - } - - @Test func missCommand() async throws { - let sepc = ResourceLoader.loadResource(named: "devnet_allconfig_spec.json")!.path() - let input = CommandInput(arguments: ["Boka", "-m", sepc]) - await #expect(throws: Error.self) { - try await console.run(boka, input: input) - } - } - +struct BokaTests { @Test func commandWithWrongFilePath() async throws { let sepc = "/path/to/wrong/file.json" - let input = CommandInput(arguments: ["Boka", "--config-file", sepc]) - await #expect(throws: Error.self) { - try await console.run(boka, input: input) + var boka = try Boka.parseAsRoot(["--chain", sepc]) as! Boka + await #expect(throws: GenesisError.self) { + try await boka.run() } } diff --git a/Makefile b/Makefile index d678166c..6c261c65 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,11 @@ format-all: format format-cargo .PHONY: format-clang format-clang: find . \( -name "*.c" -o -name "helpers.h" \) -exec clang-format -i {} + - + .PHONY: run run: githooks - swift run --package-path Boka + swift run --package-path Boka Boka --validator + +.PHONY: devnet +devnet: + ./scripts/devnet.sh diff --git a/Node/Package.resolved b/Node/Package.resolved index f3d1850c..cdee67bc 100644 --- a/Node/Package.resolved +++ b/Node/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "00297367819962d99ceeff8fa5ed585a6024d83f81bd714777c40d0fc4c7dad0", + "originHash" : "7dcc3c8ac80d868e86196be8e7bbe367d44c35130f5b194c66eec8cb296fa12b", "pins" : [ { "identity" : "async-channels", "kind" : "remoteSourceControl", "location" : "https://github.com/gh123man/Async-Channels.git", "state" : { - "revision" : "37d32cfc70f08b72a38a2c40f65338ee023afa45", - "version" : "1.0.1" + "revision" : "e4c71cd0364532d34b27e76f4ab7229abaaaefb4", + "version" : "1.0.2" } }, { diff --git a/Node/Sources/Node/Genesis.swift b/Node/Sources/Node/Genesis.swift index 32419add..f81827e2 100644 --- a/Node/Sources/Node/Genesis.swift +++ b/Node/Sources/Node/Genesis.swift @@ -2,8 +2,25 @@ import Blockchain import Foundation import Utils -public enum Genesis { +public enum GenesisPreset: String, Codable, CaseIterable { + case minimal case dev + case mainnet + + public var config: ProtocolConfigRef { + switch self { + case .minimal: + ProtocolConfigRef.minimal + case .dev: + ProtocolConfigRef.dev + case .mainnet: + ProtocolConfigRef.mainnet + } + } +} + +public enum Genesis { + case preset(GenesisPreset) case file(path: String) } @@ -23,24 +40,20 @@ public enum GenesisError: Error { extension Genesis { public func load() async throws -> (StateRef, BlockRef, ProtocolConfigRef) { switch self { - case .dev: - let config = ProtocolConfigRef.dev + case let .preset(preset): + let config = preset.config let (state, block) = try State.devGenesis(config: config) return (state, block, config) case let .file(path): let genesis = try readAndValidateGenesis(from: path) var config: ProtocolConfig - let preset = genesis.preset?.lowercased() - switch preset { - case "dev", "mainnet": - config = - (preset == "dev" - ? ProtocolConfigRef.dev.value : ProtocolConfigRef.mainnet.value) + if let preset = genesis.preset { + config = preset.config.value if let genesisConfig = genesis.config { config = config.merged(with: genesisConfig) } - default: - // In this case, genesis.config has been verified to be non-nil + } else { + // The decoder ensures that genesis.config is non-nil when there is no preset config = genesis.config! } let configRef = Ref(config) @@ -63,10 +76,6 @@ extension Genesis { if genesis.state.isEmpty { throw GenesisError.invalidFormat("Invalid or missing 'state'") } - let preset = genesis.preset?.lowercased() - if preset != nil, !["dev", "mainnet"].contains(preset!) { - throw GenesisError.invalidFormat("Invalid preset value. Must be 'dev' or 'mainnet'.") - } } func readAndValidateGenesis(from filePath: String) throws -> GenesisData { @@ -98,22 +107,23 @@ extension KeyedDecodingContainer { } } -struct GenesisData: Sendable, Codable { +struct GenesisData: Codable { var name: String var id: String var bootnodes: [String] - var preset: String? + var preset: GenesisPreset? var config: ProtocolConfig? // TODO: check & deal with state var state: String + // ensure one of preset or config is present init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) id = try container.decode(String.self, forKey: .id) bootnodes = try container.decode([String].self, forKey: .bootnodes) - preset = try container.decodeIfPresent(String.self, forKey: .preset) - if preset == nil || !["dev", "mainnet"].contains(preset) { + preset = try container.decodeIfPresent(GenesisPreset.self, forKey: .preset) + if preset == nil { config = try container.decode(ProtocolConfig.self, forKey: .config, required: true) } else { config = try container.decodeIfPresent(ProtocolConfig.self, forKey: .config, required: false) diff --git a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3b97f812..be679657 100644 --- a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "15c8990c2a71904ae0fb2311d87b37d53447f70bb359a55f0a30906bb5dcd56f", + "originHash" : "c109234482ca746f1aec9c865330521dcffc1ddb2a6ca3220ec46803cb7f10b0", "pins" : [ { "identity" : "async-channels", "kind" : "remoteSourceControl", "location" : "https://github.com/gh123man/Async-Channels.git", "state" : { - "revision" : "37d32cfc70f08b72a38a2c40f65338ee023afa45", - "version" : "1.0.1" + "revision" : "e4c71cd0364532d34b27e76f4ab7229abaaaefb4", + "version" : "1.0.2" } }, { @@ -220,7 +220,7 @@ { "identity" : "swift-numerics", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics.git", + "location" : "https://github.com/apple/swift-numerics", "state" : { "branch" : "main", "revision" : "e30276bff2ff5ed80566fbdca49f50aa160b0e83" diff --git a/scripts/devnet.sh b/scripts/devnet.sh new file mode 100755 index 00000000..9cc3382d --- /dev/null +++ b/scripts/devnet.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -e + +swift build --package-path Boka + +bin_path=$(swift build --package-path Boka --show-bin-path)/Boka + +create_node() { + local node_number=$1 + local port=$((9000 + node_number)) + local p2p_port=$((19000 + node_number)) + + tmux send-keys -t boka "$bin_path --chain=minimal --rpc 127.0.0.1:$port --validator --dev-seed $node_number --p2p 127.0.0.1:$p2p_port --peers=127.0.0.1:19001 --peers=127.0.0.1:19002 --peers=127.0.0.1:19003" C-m +} + +# Start a new tmux session +tmux new-session -d -s boka + +# Split the window into 3 panes +tmux split-window -v -t boka +tmux split-window -v -t boka + +# Create nodes in each pane +for i in {1..3} +do + tmux select-pane -t $((i-1)) + create_node $i +done + +# Attach to the tmux session, -CC for iTerm2 integration +tmux -CC attach-session -t boka