Skip to content

Commit c8815ec

Browse files
authored
add jip chainsepc (#342)
1 parent b6caf63 commit c8815ec

File tree

11 files changed

+325
-185
lines changed

11 files changed

+325
-185
lines changed

Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,4 +749,89 @@ extension ProtocolConfig {
749749
UInt32(ticketSubmissionEndSlot),
750750
)
751751
}
752+
753+
/// Decode ProtocolConfig from encoded parameters.
754+
public static func decode(protocolParameters: Data) throws -> ProtocolConfig {
755+
let decoder = JamDecoder(data: protocolParameters, config: ProtocolConfigRef.minimal)
756+
757+
let additionalMinBalancePerStateItem = try decoder.decode(UInt64.self)
758+
let additionalMinBalancePerStateByte = try decoder.decode(UInt64.self)
759+
let serviceMinBalance = try decoder.decode(UInt64.self)
760+
let totalNumberOfCores = try decoder.decode(UInt16.self)
761+
let preimagePurgePeriod = try decoder.decode(UInt32.self)
762+
let epochLength = try decoder.decode(UInt32.self)
763+
let workReportAccumulationGas = try decoder.decode(Gas.self)
764+
let workPackageIsAuthorizedGas = try decoder.decode(Gas.self)
765+
let workPackageRefineGas = try decoder.decode(Gas.self)
766+
let totalAccumulationGas = try decoder.decode(Gas.self)
767+
let recentHistorySize = try decoder.decode(UInt16.self)
768+
let maxWorkItems = try decoder.decode(UInt16.self)
769+
let maxDepsInWorkReport = try decoder.decode(UInt16.self)
770+
let maxTicketsPerExtrinsic = try decoder.decode(UInt16.self)
771+
let maxLookupAnchorAge = try decoder.decode(UInt32.self)
772+
let ticketEntriesPerValidator = try decoder.decode(UInt16.self)
773+
let maxAuthorizationsPoolItems = try decoder.decode(UInt16.self)
774+
let slotPeriodSeconds = try decoder.decode(UInt16.self)
775+
let maxAuthorizationsQueueItems = try decoder.decode(UInt16.self)
776+
let coreAssignmentRotationPeriod = try decoder.decode(UInt16.self)
777+
let maxWorkPackageExtrinsics = try decoder.decode(UInt16.self)
778+
let preimageReplacementPeriod = try decoder.decode(UInt16.self)
779+
let totalNumberOfValidators = try decoder.decode(UInt16.self)
780+
let maxIsAuthorizedCodeSize = try decoder.decode(UInt32.self)
781+
let maxEncodedWorkPackageSize = try decoder.decode(UInt32.self)
782+
let maxServiceCodeSize = try decoder.decode(UInt32.self)
783+
let erasureCodedPieceSize = try decoder.decode(UInt32.self)
784+
let maxWorkPackageImports = try decoder.decode(UInt32.self)
785+
let erasureCodedSegmentSize = try decoder.decode(UInt32.self)
786+
let maxWorkReportBlobSize = try decoder.decode(UInt32.self)
787+
let transferMemoSize = try decoder.decode(UInt32.self)
788+
let maxWorkPackageExports = try decoder.decode(UInt32.self)
789+
let ticketSubmissionEndSlot = try decoder.decode(UInt32.self)
790+
791+
let protocolConfig = ProtocolConfig(
792+
auditTranchePeriod: 8, // A = 8
793+
additionalMinBalancePerStateItem: Int(additionalMinBalancePerStateItem),
794+
additionalMinBalancePerStateByte: Int(additionalMinBalancePerStateByte),
795+
serviceMinBalance: Int(serviceMinBalance),
796+
totalNumberOfCores: Int(totalNumberOfCores),
797+
preimagePurgePeriod: Int(preimagePurgePeriod),
798+
epochLength: Int(epochLength),
799+
auditBiasFactor: 2, // F = 2
800+
workReportAccumulationGas: workReportAccumulationGas,
801+
workPackageIsAuthorizedGas: workPackageIsAuthorizedGas,
802+
workPackageRefineGas: workPackageRefineGas,
803+
totalAccumulationGas: totalAccumulationGas,
804+
recentHistorySize: Int(recentHistorySize),
805+
maxWorkItems: Int(maxWorkItems),
806+
maxDepsInWorkReport: Int(maxDepsInWorkReport),
807+
maxTicketsPerExtrinsic: Int(maxTicketsPerExtrinsic),
808+
maxLookupAnchorAge: Int(maxLookupAnchorAge),
809+
transferMemoSize: Int(transferMemoSize),
810+
ticketEntriesPerValidator: Int(ticketEntriesPerValidator),
811+
maxAuthorizationsPoolItems: Int(maxAuthorizationsPoolItems),
812+
slotPeriodSeconds: Int(slotPeriodSeconds),
813+
maxAuthorizationsQueueItems: Int(maxAuthorizationsQueueItems),
814+
coreAssignmentRotationPeriod: Int(coreAssignmentRotationPeriod),
815+
maxAccumulationQueueItems: 1024, // S = 1024
816+
maxWorkPackageExtrinsics: Int(maxWorkPackageExtrinsics),
817+
maxIsAuthorizedCodeSize: Int(maxIsAuthorizedCodeSize),
818+
maxServiceCodeSize: Int(maxServiceCodeSize),
819+
preimageReplacementPeriod: Int(preimageReplacementPeriod),
820+
totalNumberOfValidators: Int(totalNumberOfValidators),
821+
erasureCodedPieceSize: Int(erasureCodedPieceSize),
822+
maxWorkPackageImports: Int(maxWorkPackageImports),
823+
maxWorkPackageExports: Int(maxWorkPackageExports),
824+
maxEncodedWorkPackageSize: Int(maxEncodedWorkPackageSize),
825+
segmentSize: 4104, // WG = WP*WE = 4104
826+
maxWorkReportBlobSize: Int(maxWorkReportBlobSize),
827+
erasureCodedSegmentSize: Int(erasureCodedSegmentSize),
828+
ticketSubmissionEndSlot: Int(ticketSubmissionEndSlot),
829+
pvmDynamicAddressAlignmentFactor: 2, // ZA = 2
830+
pvmProgramInitInputDataSize: 1 << 24, // ZI = 2^24
831+
pvmProgramInitZoneSize: 1 << 16, // ZZ = 2^16
832+
pvmMemoryPageSize: 1 << 12 // ZP = 2^12
833+
)
834+
835+
return protocolConfig
836+
}
752837
}

Boka/Sources/Generate.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,39 @@ import Foundation
44
import Node
55
import Utils
66

7+
extension GenesisPreset: @retroactive ExpressibleByArgument {}
8+
79
struct Generate: AsyncParsableCommand {
810
static let configuration = CommandConfiguration(
9-
abstract: "Generate new chainspec file"
11+
abstract: "Generate a JIP 4 chainspec file"
1012
)
1113

1214
@Argument(help: "output file")
1315
var output: String
1416

15-
@Option(name: .long, help: "A preset config or path to chain config file.")
16-
var chain: Genesis = .preset(.minimal)
17+
@Option(name: .long, help: "A JAM preset config.")
18+
var config: GenesisPreset = .minimal
1719

18-
@Option(name: .long, help: "The chain name.")
19-
var name: String = "Devnet"
20+
@Option(name: .long, help: "Path to existing chainspec file to use. This has priority over the preset config.")
21+
var chainspec: String?
2022

2123
@Option(name: .long, help: "The chain id.")
22-
var id: String = "dev"
24+
var id: String?
2325

2426
func run() async throws {
25-
let chainspec = try await chain.load()
26-
let data = try chainspec.encode()
27+
let genesis: Genesis = if let chainspecPath = chainspec {
28+
.file(path: chainspecPath)
29+
} else {
30+
.preset(config)
31+
}
32+
33+
var chainSpec = try await genesis.load()
34+
35+
if let customId = id {
36+
chainSpec.id = customId
37+
}
38+
39+
let data = try chainSpec.encode()
2740
try data.write(to: URL(fileURLWithPath: output))
2841

2942
print("Chainspec generated at \(output)")

Node/Sources/Node/ChainSpec.swift

Lines changed: 98 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,89 +3,60 @@ import Codec
33
import Foundation
44
import Utils
55

6-
extension KeyedDecodingContainer {
7-
func decode(_: ProtocolConfig.Type, forKey key: K, required: Bool = true) throws -> ProtocolConfig {
8-
let nestedDecoder = try superDecoder(forKey: key)
9-
return try ProtocolConfig(from: nestedDecoder, required)
10-
}
11-
12-
func decodeIfPresent(_: ProtocolConfig.Type, forKey key: K, required: Bool = false) throws -> ProtocolConfig? {
13-
guard contains(key) else { return nil }
14-
let nestedDecoder = try superDecoder(forKey: key)
15-
return try ProtocolConfig(from: nestedDecoder, required)
16-
}
17-
}
18-
19-
private func mergeConfig(preset: GenesisPreset?, config: ProtocolConfig?) throws -> ProtocolConfigRef {
20-
if let preset {
21-
let ret = preset.config.value
22-
if let genesisConfig = config {
23-
return Ref(ret.merged(with: genesisConfig))
24-
}
25-
return Ref(ret)
26-
}
27-
if let config {
28-
return Ref(config)
29-
}
30-
throw GenesisError.invalidFormat("One of 'preset' or 'config' is required")
31-
}
32-
336
public struct ChainSpec: Codable, Equatable {
34-
public var name: String
357
public var id: String
36-
public var bootnodes: [String]
37-
public var preset: GenesisPreset?
38-
public var config: ProtocolConfig?
39-
public var block: Data
40-
public var state: [String: Data]
8+
public var bootnodes: [String]?
9+
public var genesisHeader: Data
10+
public var genesisState: [String: Data]
11+
public var protocolParameters: Data
12+
13+
private enum CodingKeys: String, CodingKey {
14+
case id
15+
case bootnodes
16+
case genesisHeader = "genesis_header"
17+
case genesisState = "genesis_state"
18+
case protocolParameters = "protocol_parameters"
19+
}
4120

4221
public init(
43-
name: String,
4422
id: String,
45-
bootnodes: [String],
46-
preset: GenesisPreset?,
47-
config: ProtocolConfig?,
48-
block: Data,
49-
state: [String: Data]
23+
bootnodes: [String]? = nil,
24+
genesisHeader: Data,
25+
genesisState: [String: Data],
26+
protocolParameters: Data
5027
) {
51-
self.name = name
5228
self.id = id
5329
self.bootnodes = bootnodes
54-
self.preset = preset
55-
self.config = config
56-
self.block = block
57-
self.state = state
30+
self.genesisHeader = genesisHeader
31+
self.genesisState = genesisState
32+
self.protocolParameters = protocolParameters
5833
}
5934

6035
public init(from decoder: Decoder) throws {
6136
let container = try decoder.container(keyedBy: CodingKeys.self)
62-
name = try container.decode(String.self, forKey: .name)
6337
id = try container.decode(String.self, forKey: .id)
64-
bootnodes = try container.decode([String].self, forKey: .bootnodes)
65-
preset = try container.decodeIfPresent(GenesisPreset.self, forKey: .preset)
66-
if preset == nil {
67-
config = try container.decode(ProtocolConfig.self, forKey: .config, required: true)
68-
} else {
69-
config = try container.decodeIfPresent(ProtocolConfig.self, forKey: .config, required: false)
70-
}
71-
72-
try decoder.setConfig(mergeConfig(preset: preset, config: config))
38+
bootnodes = try container.decodeIfPresent([String].self, forKey: .bootnodes)
39+
genesisHeader = try container.decode(Data.self, forKey: .genesisHeader)
40+
genesisState = try container.decode(Dictionary<String, Data>.self, forKey: .genesisState)
41+
protocolParameters = try container.decode(Data.self, forKey: .protocolParameters)
7342

74-
block = try container.decode(Data.self, forKey: .block)
75-
state = try container.decode(Dictionary<String, Data>.self, forKey: .state)
43+
try decoder.setConfig(getConfig())
7644
}
7745

7846
public func getConfig() throws -> ProtocolConfigRef {
79-
try mergeConfig(preset: preset, config: config)
47+
let config = try ProtocolConfig.decode(protocolParameters: protocolParameters)
48+
return Ref(config)
8049
}
8150

8251
public func getBlock() throws -> BlockRef {
83-
try JamDecoder.decode(BlockRef.self, from: block, withConfig: getConfig())
52+
let config = try getConfig()
53+
let header = try JamDecoder.decode(Header.self, from: genesisHeader, withConfig: config)
54+
return BlockRef(Block(header: header, extrinsic: .dummy(config: config)))
8455
}
8556

8657
public func getState() throws -> [Data31: Data] {
8758
var output: [Data31: Data] = [:]
88-
for (key, value) in state {
59+
for (key, value) in genesisState {
8960
try output[Data31(fromHexString: key).unwrap()] = value
9061
}
9162
return output
@@ -110,15 +81,77 @@ public struct ChainSpec: Codable, Equatable {
11081
}
11182

11283
private func validate() throws {
113-
// Validate required fields
114-
if name.isEmpty {
115-
throw GenesisError.invalidFormat("Missing 'name'")
116-
}
11784
if id.isEmpty {
11885
throw GenesisError.invalidFormat("Missing 'id'")
11986
}
120-
if preset == nil, config == nil {
121-
throw GenesisError.invalidFormat("One of 'preset' or 'config' is required")
87+
88+
for key in genesisState.keys {
89+
guard key.count == 62 else {
90+
throw GenesisError.invalidFormat("Invalid genesisState key length: \(key) (expected 62 characters)")
91+
}
92+
guard Data(fromHexString: key) != nil else {
93+
throw GenesisError.invalidFormat("Invalid genesisState key format: \(key) (not valid hex)")
94+
}
95+
}
96+
97+
if let bootnodes {
98+
for bootnode in bootnodes {
99+
try validateBootnodeFormat(bootnode)
100+
}
101+
}
102+
}
103+
104+
private func validateBootnodeFormat(_ bootnode: String) throws {
105+
// Format: <name>@<ip>:<port>
106+
// <name> is 53-character DNS name starting with 'e' followed by base-32 encoded Ed25519 public key
107+
let components = bootnode.split(separator: "@")
108+
guard components.count == 2 else {
109+
throw GenesisError.invalidFormat("Invalid bootnode format: \(bootnode) (expected name@ip:port)")
110+
}
111+
112+
let name = String(components[0])
113+
let addressPort = String(components[1])
114+
115+
// Validate name: 53 characters, starts with 'e'
116+
guard name.count == 53, name.hasPrefix("e") else {
117+
throw GenesisError.invalidFormat("Invalid bootnode name: \(name) (expected 53 characters starting with 'e')")
118+
}
119+
120+
// Validate base-32 encoding (check for allowed characters)
121+
let base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"
122+
let nameWithoutPrefix = String(name.dropFirst())
123+
guard nameWithoutPrefix.allSatisfy({ base32Alphabet.contains($0) }) else {
124+
throw GenesisError.invalidFormat("Invalid bootnode name encoding: \(name) (not valid base-32)")
125+
}
126+
127+
// Validate ip:port format
128+
let addressComponents = addressPort.split(separator: ":")
129+
guard addressComponents.count == 2 else {
130+
throw GenesisError.invalidFormat("Invalid bootnode address: \(addressPort) (expected ip:port)")
131+
}
132+
133+
// Validate IP address format
134+
guard String(addressComponents[0]).isIpAddress() else {
135+
throw GenesisError.invalidFormat("Invalid bootnode IP address: \(addressComponents[0]) (not a valid IP address)")
122136
}
137+
138+
// Validate port is a number
139+
guard Int(addressComponents[1]) != nil else {
140+
throw GenesisError.invalidFormat("Invalid bootnode port: \(addressComponents[1]) (not a number)")
141+
}
142+
}
143+
}
144+
145+
extension String {
146+
func isIPv4() -> Bool {
147+
var sin = sockaddr_in()
148+
return withCString { cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) } == 1
123149
}
150+
151+
func isIPv6() -> Bool {
152+
var sin6 = sockaddr_in6()
153+
return withCString { cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) } == 1
154+
}
155+
156+
func isIpAddress() -> Bool { isIPv6() || isIPv4() }
124157
}

Node/Sources/Node/Genesis.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Utils
66
public enum GenesisPreset: String, Codable, CaseIterable {
77
case minimal
88
case dev
9+
case tiny
910
case mainnet
1011

1112
public var config: ProtocolConfigRef {
@@ -14,6 +15,8 @@ public enum GenesisPreset: String, Codable, CaseIterable {
1415
ProtocolConfigRef.minimal
1516
case .dev:
1617
ProtocolConfigRef.dev
18+
case .tiny:
19+
ProtocolConfigRef.tiny
1720
case .mainnet:
1821
ProtocolConfigRef.mainnet
1922
}
@@ -51,13 +54,11 @@ extension Genesis {
5154
}
5255
}
5356
return try ChainSpec(
54-
name: preset.rawValue,
5557
id: preset.rawValue,
5658
bootnodes: [],
57-
preset: preset,
58-
config: config.value,
59-
block: JamEncoder.encode(block.value),
60-
state: kv
59+
genesisHeader: JamEncoder.encode(block.value.header),
60+
genesisState: kv,
61+
protocolParameters: config.value.encoded
6162
)
6263
case let .file(path):
6364
let data = try readFile(from: path)

0 commit comments

Comments
 (0)