Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,89 @@ extension ProtocolConfig {
UInt32(ticketSubmissionEndSlot),
)
}

/// Decode ProtocolConfig from encoded parameters.
public static func decode(protocolParameters: Data) throws -> ProtocolConfig {
let decoder = JamDecoder(data: protocolParameters, config: ProtocolConfigRef.minimal)

let additionalMinBalancePerStateItem = try decoder.decode(UInt64.self)
let additionalMinBalancePerStateByte = try decoder.decode(UInt64.self)
let serviceMinBalance = try decoder.decode(UInt64.self)
let totalNumberOfCores = try decoder.decode(UInt16.self)
let preimagePurgePeriod = try decoder.decode(UInt32.self)
let epochLength = try decoder.decode(UInt32.self)
let workReportAccumulationGas = try decoder.decode(Gas.self)
let workPackageIsAuthorizedGas = try decoder.decode(Gas.self)
let workPackageRefineGas = try decoder.decode(Gas.self)
let totalAccumulationGas = try decoder.decode(Gas.self)
let recentHistorySize = try decoder.decode(UInt16.self)
let maxWorkItems = try decoder.decode(UInt16.self)
let maxDepsInWorkReport = try decoder.decode(UInt16.self)
let maxTicketsPerExtrinsic = try decoder.decode(UInt16.self)
let maxLookupAnchorAge = try decoder.decode(UInt32.self)
let ticketEntriesPerValidator = try decoder.decode(UInt16.self)
let maxAuthorizationsPoolItems = try decoder.decode(UInt16.self)
let slotPeriodSeconds = try decoder.decode(UInt16.self)
let maxAuthorizationsQueueItems = try decoder.decode(UInt16.self)
let coreAssignmentRotationPeriod = try decoder.decode(UInt16.self)
let maxWorkPackageExtrinsics = try decoder.decode(UInt16.self)
let preimageReplacementPeriod = try decoder.decode(UInt16.self)
let totalNumberOfValidators = try decoder.decode(UInt16.self)
let maxIsAuthorizedCodeSize = try decoder.decode(UInt32.self)
let maxEncodedWorkPackageSize = try decoder.decode(UInt32.self)
let maxServiceCodeSize = try decoder.decode(UInt32.self)
let erasureCodedPieceSize = try decoder.decode(UInt32.self)
let maxWorkPackageImports = try decoder.decode(UInt32.self)
let erasureCodedSegmentSize = try decoder.decode(UInt32.self)
let maxWorkReportBlobSize = try decoder.decode(UInt32.self)
let transferMemoSize = try decoder.decode(UInt32.self)
let maxWorkPackageExports = try decoder.decode(UInt32.self)
let ticketSubmissionEndSlot = try decoder.decode(UInt32.self)

let protocolConfig = ProtocolConfig(
auditTranchePeriod: 8, // A = 8
additionalMinBalancePerStateItem: Int(additionalMinBalancePerStateItem),
additionalMinBalancePerStateByte: Int(additionalMinBalancePerStateByte),
serviceMinBalance: Int(serviceMinBalance),
totalNumberOfCores: Int(totalNumberOfCores),
preimagePurgePeriod: Int(preimagePurgePeriod),
epochLength: Int(epochLength),
auditBiasFactor: 2, // F = 2
workReportAccumulationGas: workReportAccumulationGas,
workPackageIsAuthorizedGas: workPackageIsAuthorizedGas,
workPackageRefineGas: workPackageRefineGas,
totalAccumulationGas: totalAccumulationGas,
recentHistorySize: Int(recentHistorySize),
maxWorkItems: Int(maxWorkItems),
maxDepsInWorkReport: Int(maxDepsInWorkReport),
maxTicketsPerExtrinsic: Int(maxTicketsPerExtrinsic),
maxLookupAnchorAge: Int(maxLookupAnchorAge),
transferMemoSize: Int(transferMemoSize),
ticketEntriesPerValidator: Int(ticketEntriesPerValidator),
maxAuthorizationsPoolItems: Int(maxAuthorizationsPoolItems),
slotPeriodSeconds: Int(slotPeriodSeconds),
maxAuthorizationsQueueItems: Int(maxAuthorizationsQueueItems),
coreAssignmentRotationPeriod: Int(coreAssignmentRotationPeriod),
maxAccumulationQueueItems: 1024, // S = 1024
maxWorkPackageExtrinsics: Int(maxWorkPackageExtrinsics),
maxIsAuthorizedCodeSize: Int(maxIsAuthorizedCodeSize),
maxServiceCodeSize: Int(maxServiceCodeSize),
preimageReplacementPeriod: Int(preimageReplacementPeriod),
totalNumberOfValidators: Int(totalNumberOfValidators),
erasureCodedPieceSize: Int(erasureCodedPieceSize),
maxWorkPackageImports: Int(maxWorkPackageImports),
maxWorkPackageExports: Int(maxWorkPackageExports),
maxEncodedWorkPackageSize: Int(maxEncodedWorkPackageSize),
segmentSize: 4104, // WG = WP*WE = 4104
maxWorkReportBlobSize: Int(maxWorkReportBlobSize),
erasureCodedSegmentSize: Int(erasureCodedSegmentSize),
ticketSubmissionEndSlot: Int(ticketSubmissionEndSlot),
pvmDynamicAddressAlignmentFactor: 2, // ZA = 2
pvmProgramInitInputDataSize: 1 << 24, // ZI = 2^24
pvmProgramInitZoneSize: 1 << 16, // ZZ = 2^16
pvmMemoryPageSize: 1 << 12 // ZP = 2^12
)

return protocolConfig
}
}
29 changes: 21 additions & 8 deletions Boka/Sources/Generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,39 @@ import Foundation
import Node
import Utils

extension GenesisPreset: @retroactive ExpressibleByArgument {}

struct Generate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Generate new chainspec file"
abstract: "Generate a JIP 4 chainspec file"
)

@Argument(help: "output file")
var output: String

@Option(name: .long, help: "A preset config or path to chain config file.")
var chain: Genesis = .preset(.minimal)
@Option(name: .long, help: "A JAM preset config.")
var config: GenesisPreset = .minimal

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

@Option(name: .long, help: "The chain id.")
var id: String = "dev"
var id: String?

func run() async throws {
let chainspec = try await chain.load()
let data = try chainspec.encode()
let genesis: Genesis = if let chainspecPath = chainspec {
.file(path: chainspecPath)
} else {
.preset(config)
}

var chainSpec = try await genesis.load()

if let customId = id {
chainSpec.id = customId
}

let data = try chainSpec.encode()
try data.write(to: URL(fileURLWithPath: output))

print("Chainspec generated at \(output)")
Expand Down
163 changes: 98 additions & 65 deletions Node/Sources/Node/ChainSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,60 @@ import Codec
import Foundation
import Utils

extension KeyedDecodingContainer {
func decode(_: ProtocolConfig.Type, forKey key: K, required: Bool = true) throws -> ProtocolConfig {
let nestedDecoder = try superDecoder(forKey: key)
return try ProtocolConfig(from: nestedDecoder, required)
}

func decodeIfPresent(_: ProtocolConfig.Type, forKey key: K, required: Bool = false) throws -> ProtocolConfig? {
guard contains(key) else { return nil }
let nestedDecoder = try superDecoder(forKey: key)
return try ProtocolConfig(from: nestedDecoder, required)
}
}

private func mergeConfig(preset: GenesisPreset?, config: ProtocolConfig?) throws -> ProtocolConfigRef {
if let preset {
let ret = preset.config.value
if let genesisConfig = config {
return Ref(ret.merged(with: genesisConfig))
}
return Ref(ret)
}
if let config {
return Ref(config)
}
throw GenesisError.invalidFormat("One of 'preset' or 'config' is required")
}

public struct ChainSpec: Codable, Equatable {
public var name: String
public var id: String
public var bootnodes: [String]
public var preset: GenesisPreset?
public var config: ProtocolConfig?
public var block: Data
public var state: [String: Data]
public var bootnodes: [String]?
public var genesisHeader: Data
public var genesisState: [String: Data]
public var protocolParameters: Data

private enum CodingKeys: String, CodingKey {
case id
case bootnodes
case genesisHeader = "genesis_header"
case genesisState = "genesis_state"
case protocolParameters = "protocol_parameters"
}

public init(
name: String,
id: String,
bootnodes: [String],
preset: GenesisPreset?,
config: ProtocolConfig?,
block: Data,
state: [String: Data]
bootnodes: [String]? = nil,
genesisHeader: Data,
genesisState: [String: Data],
protocolParameters: Data
) {
self.name = name
self.id = id
self.bootnodes = bootnodes
self.preset = preset
self.config = config
self.block = block
self.state = state
self.genesisHeader = genesisHeader
self.genesisState = genesisState
self.protocolParameters = protocolParameters
}

public 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(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)
}

try decoder.setConfig(mergeConfig(preset: preset, config: config))
bootnodes = try container.decodeIfPresent([String].self, forKey: .bootnodes)
genesisHeader = try container.decode(Data.self, forKey: .genesisHeader)
genesisState = try container.decode(Dictionary<String, Data>.self, forKey: .genesisState)
protocolParameters = try container.decode(Data.self, forKey: .protocolParameters)

block = try container.decode(Data.self, forKey: .block)
state = try container.decode(Dictionary<String, Data>.self, forKey: .state)
try decoder.setConfig(getConfig())
}

public func getConfig() throws -> ProtocolConfigRef {
try mergeConfig(preset: preset, config: config)
let config = try ProtocolConfig.decode(protocolParameters: protocolParameters)
return Ref(config)
}

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

public func getState() throws -> [Data31: Data] {
var output: [Data31: Data] = [:]
for (key, value) in state {
for (key, value) in genesisState {
try output[Data31(fromHexString: key).unwrap()] = value
}
return output
Expand All @@ -110,15 +81,77 @@ public struct ChainSpec: Codable, Equatable {
}

private func validate() throws {
// Validate required fields
if name.isEmpty {
throw GenesisError.invalidFormat("Missing 'name'")
}
if id.isEmpty {
throw GenesisError.invalidFormat("Missing 'id'")
}
if preset == nil, config == nil {
throw GenesisError.invalidFormat("One of 'preset' or 'config' is required")

for key in genesisState.keys {
guard key.count == 62 else {
throw GenesisError.invalidFormat("Invalid genesisState key length: \(key) (expected 62 characters)")
}
guard Data(fromHexString: key) != nil else {
throw GenesisError.invalidFormat("Invalid genesisState key format: \(key) (not valid hex)")
}
}

if let bootnodes {
for bootnode in bootnodes {
try validateBootnodeFormat(bootnode)
}
}
}

private func validateBootnodeFormat(_ bootnode: String) throws {
// Format: <name>@<ip>:<port>
// <name> is 53-character DNS name starting with 'e' followed by base-32 encoded Ed25519 public key
let components = bootnode.split(separator: "@")
guard components.count == 2 else {
throw GenesisError.invalidFormat("Invalid bootnode format: \(bootnode) (expected name@ip:port)")
}

let name = String(components[0])
let addressPort = String(components[1])

// Validate name: 53 characters, starts with 'e'
guard name.count == 53, name.hasPrefix("e") else {
throw GenesisError.invalidFormat("Invalid bootnode name: \(name) (expected 53 characters starting with 'e')")
}

// Validate base-32 encoding (check for allowed characters)
let base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"
let nameWithoutPrefix = String(name.dropFirst())
guard nameWithoutPrefix.allSatisfy({ base32Alphabet.contains($0) }) else {
throw GenesisError.invalidFormat("Invalid bootnode name encoding: \(name) (not valid base-32)")
}

// Validate ip:port format
let addressComponents = addressPort.split(separator: ":")
guard addressComponents.count == 2 else {
throw GenesisError.invalidFormat("Invalid bootnode address: \(addressPort) (expected ip:port)")
}

// Validate IP address format
guard String(addressComponents[0]).isIpAddress() else {
throw GenesisError.invalidFormat("Invalid bootnode IP address: \(addressComponents[0]) (not a valid IP address)")
}

// Validate port is a number
guard Int(addressComponents[1]) != nil else {
throw GenesisError.invalidFormat("Invalid bootnode port: \(addressComponents[1]) (not a number)")
}
}
}

extension String {
func isIPv4() -> Bool {
var sin = sockaddr_in()
return withCString { cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) } == 1
}

func isIPv6() -> Bool {
var sin6 = sockaddr_in6()
return withCString { cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) } == 1
}

func isIpAddress() -> Bool { isIPv6() || isIPv4() }
}
11 changes: 6 additions & 5 deletions Node/Sources/Node/Genesis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Utils
public enum GenesisPreset: String, Codable, CaseIterable {
case minimal
case dev
case tiny
case mainnet

public var config: ProtocolConfigRef {
Expand All @@ -14,6 +15,8 @@ public enum GenesisPreset: String, Codable, CaseIterable {
ProtocolConfigRef.minimal
case .dev:
ProtocolConfigRef.dev
case .tiny:
ProtocolConfigRef.tiny
case .mainnet:
ProtocolConfigRef.mainnet
}
Expand Down Expand Up @@ -51,13 +54,11 @@ extension Genesis {
}
}
return try ChainSpec(
name: preset.rawValue,
id: preset.rawValue,
bootnodes: [],
preset: preset,
config: config.value,
block: JamEncoder.encode(block.value),
state: kv
genesisHeader: JamEncoder.encode(block.value.header),
genesisState: kv,
protocolParameters: config.value.encoded
)
case let .file(path):
let data = try readFile(from: path)
Expand Down
Loading