@@ -3,89 +3,60 @@ import Codec
33import Foundation
44import 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-
336public 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}
0 commit comments