Skip to content

Commit 512fc5e

Browse files
committed
subcommand generate to generate chainspec file
1 parent e2e9beb commit 512fc5e

File tree

4 files changed

+179
-38
lines changed

4 files changed

+179
-38
lines changed

Blockchain/Sources/Blockchain/Types/State.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ extension State {
126126
public var lastBlockHash: Data32 {
127127
recentHistory.items.last.map(\.headerHash)!
128128
}
129+
130+
public func asRef() -> StateRef {
131+
StateRef(self)
132+
}
129133
}
130134

131135
extension State: Dummy {

Boka/Sources/Boka.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ enum MaybeEnabled<T: ExpressibleByArgument>: ExpressibleByArgument {
5151
struct Boka: AsyncParsableCommand {
5252
static let configuration = CommandConfiguration(
5353
abstract: "JAM built with Swift",
54-
version: "0.0.1"
54+
version: "0.0.1",
55+
subcommands: [Generate.self]
5556
)
5657

5758
@Option(name: .shortAndLong, help: "Base path to database files.")

Boka/Sources/Generate.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import ArgumentParser
2+
import Codec
3+
import Foundation
4+
import Node
5+
import Utils
6+
7+
enum OutputFormat: String, ExpressibleByArgument {
8+
case json
9+
case binary
10+
}
11+
12+
struct Generate: AsyncParsableCommand {
13+
static let configuration = CommandConfiguration(
14+
abstract: "Generate new chainspec file"
15+
)
16+
17+
@Argument(help: "output file")
18+
var output: String
19+
20+
@Option(name: .long, help: "A preset config or path to chain config file.")
21+
var chain: Genesis = .preset(.minimal)
22+
23+
@Option(name: .long, help: "The output format. json or binary.")
24+
var format: OutputFormat = .json
25+
26+
@Option(name: .long, help: "The chain name.")
27+
var name: String = "Devnet"
28+
29+
@Option(name: .long, help: "The chain id.")
30+
var id: String = "dev"
31+
32+
func run() async throws {
33+
let (state, block, config) = try await chain.load()
34+
35+
let encoder = JSONEncoder()
36+
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
37+
encoder.dataEncodingStrategy = .hex
38+
39+
let genesis: any Encodable = switch format {
40+
case .json:
41+
GenesisData(
42+
name: name,
43+
id: id,
44+
bootnodes: [],
45+
preset: nil,
46+
config: config.value,
47+
block: block.value,
48+
state: state.value
49+
)
50+
case .binary:
51+
try GenesisDataBinary(
52+
name: name,
53+
id: id,
54+
bootnodes: [],
55+
preset: nil,
56+
config: config.value,
57+
block: JamEncoder.encode(block),
58+
state: JamEncoder.encode(state)
59+
)
60+
}
61+
62+
let data = try encoder.encode(genesis)
63+
try data.write(to: URL(fileURLWithPath: output))
64+
65+
print("Chainspec generated at \(output)")
66+
}
67+
}

Node/Sources/Node/Genesis.swift

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Blockchain
2+
import Codec
23
import Foundation
34
import Utils
45

@@ -57,41 +58,50 @@ extension Genesis {
5758
config = genesis.config!
5859
}
5960
let configRef = Ref(config)
60-
let (state, block) = try State.devGenesis(config: configRef)
61+
let state = genesis.state.asRef()
62+
let block = genesis.block.asRef()
63+
6164
return (state, block, configRef)
6265
}
6366
}
6467

6568
private func validate(_ genesis: GenesisData) throws {
6669
// Validate required fields
6770
if genesis.name.isEmpty {
68-
throw GenesisError.invalidFormat("Invalid or missing 'name'")
71+
throw GenesisError.invalidFormat("Missing 'name'")
6972
}
7073
if genesis.id.isEmpty {
71-
throw GenesisError.invalidFormat("Invalid or missing 'id'")
72-
}
73-
if genesis.bootnodes.isEmpty {
74-
throw GenesisError.invalidFormat("Invalid or missing 'bootnodes'")
74+
throw GenesisError.invalidFormat("Missing 'id'")
7575
}
76-
if genesis.state.isEmpty {
77-
throw GenesisError.invalidFormat("Invalid or missing 'state'")
76+
if genesis.preset == nil, genesis.config == nil {
77+
throw GenesisError.invalidFormat("One of 'preset' or 'config' is required")
7878
}
7979
}
8080

81-
func readAndValidateGenesis(from filePath: String) throws -> GenesisData {
81+
private func readFile(from filePath: String) throws -> Data {
8282
do {
8383
let fileContents = try String(contentsOfFile: filePath, encoding: .utf8)
84-
let data = fileContents.data(using: .utf8)!
85-
let decoder = JSONDecoder()
86-
let genesis = try decoder.decode(GenesisData.self, from: data)
87-
try validate(genesis)
88-
return genesis
89-
} catch let error as GenesisError {
90-
throw error
84+
return fileContents.data(using: .utf8)!
9185
} catch {
9286
throw GenesisError.fileReadError(error)
9387
}
9488
}
89+
90+
private func parseGenesis(from data: Data) throws -> GenesisData {
91+
let decoder = JSONDecoder()
92+
if let genesisData = try? decoder.decode(GenesisData.self, from: data) {
93+
return genesisData
94+
}
95+
let genesisData = try decoder.decode(GenesisDataBinary.self, from: data)
96+
return try genesisData.toGenesisData()
97+
}
98+
99+
func readAndValidateGenesis(from filePath: String) throws -> GenesisData {
100+
let data = try readFile(from: filePath)
101+
let genesis = try parseGenesis(from: data)
102+
try validate(genesis)
103+
return genesis
104+
}
95105
}
96106

97107
extension KeyedDecodingContainer {
@@ -107,27 +117,86 @@ extension KeyedDecodingContainer {
107117
}
108118
}
109119

110-
struct GenesisData: Codable {
111-
var name: String
112-
var id: String
113-
var bootnodes: [String]
114-
var preset: GenesisPreset?
115-
var config: ProtocolConfig?
116-
// TODO: check & deal with state
117-
var state: String
118-
119-
// ensure one of preset or config is present
120-
init(from decoder: Decoder) throws {
121-
let container = try decoder.container(keyedBy: CodingKeys.self)
122-
name = try container.decode(String.self, forKey: .name)
123-
id = try container.decode(String.self, forKey: .id)
124-
bootnodes = try container.decode([String].self, forKey: .bootnodes)
125-
preset = try container.decodeIfPresent(GenesisPreset.self, forKey: .preset)
126-
if preset == nil {
127-
config = try container.decode(ProtocolConfig.self, forKey: .config, required: true)
128-
} else {
129-
config = try container.decodeIfPresent(ProtocolConfig.self, forKey: .config, required: false)
120+
private func getConfig(preset: GenesisPreset?, config: ProtocolConfig?) throws -> ProtocolConfig {
121+
if let preset {
122+
let ret = preset.config.value
123+
if let genesisConfig = config {
124+
return ret.merged(with: genesisConfig)
130125
}
131-
state = try container.decode(String.self, forKey: .state)
126+
return ret
127+
}
128+
if let config {
129+
return config
130+
}
131+
throw GenesisError.invalidFormat("One of 'preset' or 'config' is required")
132+
}
133+
134+
public struct GenesisData: Codable {
135+
public var name: String
136+
public var id: String
137+
public var bootnodes: [String]
138+
public var preset: GenesisPreset?
139+
public var config: ProtocolConfig?
140+
public var block: Block
141+
public var state: State
142+
143+
public init(
144+
name: String,
145+
id: String,
146+
bootnodes: [String],
147+
preset: GenesisPreset?,
148+
config: ProtocolConfig?,
149+
block: Block,
150+
state: State
151+
) {
152+
self.name = name
153+
self.id = id
154+
self.bootnodes = bootnodes
155+
self.preset = preset
156+
self.config = config
157+
self.block = block
158+
self.state = state
159+
}
160+
}
161+
162+
public struct GenesisDataBinary: Codable {
163+
public var name: String
164+
public var id: String
165+
public var bootnodes: [String]
166+
public var preset: GenesisPreset?
167+
public var config: ProtocolConfig?
168+
public var block: Data
169+
public var state: Data
170+
171+
public init(
172+
name: String,
173+
id: String,
174+
bootnodes: [String],
175+
preset: GenesisPreset?,
176+
config: ProtocolConfig?,
177+
block: Data,
178+
state: Data
179+
) {
180+
self.name = name
181+
self.id = id
182+
self.bootnodes = bootnodes
183+
self.preset = preset
184+
self.config = config
185+
self.block = block
186+
self.state = state
187+
}
188+
189+
public func toGenesisData() throws -> GenesisData {
190+
let block = try JamDecoder(data: block, config: config).decode(Block.self)
191+
let state = try JamDecoder(data: state, config: config).decode(State.self)
192+
return GenesisData(
193+
name: name,
194+
id: id,
195+
bootnodes: bootnodes,
196+
preset: preset,
197+
config: config,
198+
block: block,
199+
state: state
200+
)
132201
}
133202
}

0 commit comments

Comments
 (0)