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 c4ca695a..d527cf1c 100644
--- a/Boka/Package.resolved
+++ b/Boka/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "cee187bf5c40292c311294a895cbf00f2bef24f9ce8e07d377d38db947218bc0",
+ "originHash" : "60a8204ece9450dc69d2c41c6386c74b8d08e6a44261244fb5adeadc19c30e03",
"pins" : [
{
"identity" : "async-channels",
@@ -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 346b6588..be679657 100644
--- a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "a1c5a905aa50d79012ebf1996304b1c1eb89ff484b4c2fb93ed061eca43155f6",
+ "originHash" : "c109234482ca746f1aec9c865330521dcffc1ddb2a6ca3220ec46803cb7f10b0",
"pins" : [
{
"identity" : "async-channels",
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
- "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6",
- "version" : "1.3.0"
+ "revision" : "df5d2fcd22e3f480e3ef85bf23e277a4a0ef524d",
+ "version" : "1.2.0"
}
},
{
@@ -114,8 +114,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-certificates.git",
"state" : {
- "revision" : "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94",
- "version" : "1.6.1"
+ "revision" : "2f797305c1b5b982acaa6005d8a9f970cc4e97ff",
+ "version" : "1.5.0"
}
},
{
@@ -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