diff --git a/Blockchain/Sources/Blockchain/Blockchain.swift b/Blockchain/Sources/Blockchain/Blockchain.swift index 42a8e019..181227ba 100644 --- a/Blockchain/Sources/Blockchain/Blockchain.swift +++ b/Blockchain/Sources/Blockchain/Blockchain.swift @@ -40,7 +40,7 @@ public final class Blockchain: ServiceBase, @unchecked Sendable { try await withSpan("importBlock") { span in span.attributes.blockHash = block.hash.description - let runtime = Runtime(config: config) + let runtime = try Runtime(config: config, ancestry: .init(config: config)) let parent = try await dataProvider.getState(hash: block.header.parentHash) let stateRoot = await parent.value.stateRoot let timeslot = timeProvider.getTime().timeToTimeslot(config: config) diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/AccumulateFunction.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/AccumulateFunction.swift index 458205c7..df9995b9 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/AccumulateFunction.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/AccumulateFunction.swift @@ -71,7 +71,7 @@ public struct AccumulateState: Sendable { public func copy() -> AccumulateState { AccumulateState( - accounts: ServiceAccountsMutRef(accounts.value), + accounts: ServiceAccountsMutRef(copying: accounts), validatorQueue: validatorQueue, authorizationQueue: authorizationQueue, manager: manager, diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift index 871d7605..ae7a3774 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift @@ -275,12 +275,15 @@ extension Accumulation { logger.debug("[∆*] services to accumulate: \(Array(uniqueServices))") let serviceBatches = sortServicesToBatches(services: uniqueServices) + logger.debug("[∆*] service batches: \(serviceBatches)") for serviceBatch in serviceBatches { logger.debug("[∆*] processing batch: \(serviceBatch)") let batchState = currentState + batchState.accounts.clearRecordedChanges() + let batchResults = try await withThrowingTaskGroup( of: (ServiceIndex, AccumulationResult).self, returning: [(ServiceIndex, AccumulationResult)].self diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift index 8b0697fd..56bdbc4d 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift @@ -118,7 +118,8 @@ extension Guaranteeing { public func update( config: ProtocolConfigRef, timeslot: TimeslotIndex, - extrinsic: ExtrinsicGuarantees + extrinsic: ExtrinsicGuarantees, + ancestry: ConfigLimitedSizeArray? ) async throws(GuaranteeingError) -> ( newReports: ConfigFixedSizeArray< ReportItem?, @@ -225,6 +226,17 @@ extension Guaranteeing { throw .invalidContext } + if let ancestry { + let lookupTimeslot = UInt32(context.lookupAnchor.timeslot) + let lookupHeaderHash = context.lookupAnchor.headerHash + + guard ancestry.array.contains(where: { item in + item.timeslot == lookupTimeslot && item.headerHash == lookupHeaderHash + }) else { + throw .invalidContext + } + } + for prerequisiteWorkPackage in context.prerequisiteWorkPackages.union(report.lookup.keys) { guard recentWorkPackageHashes.contains(prerequisiteWorkPackage) || workPackageHashes.contains(prerequisiteWorkPackage) diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift index d8af851c..a7cd98eb 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift @@ -3,6 +3,16 @@ import Foundation import TracingUtils import Utils +public struct AncestryItem: Codable { + public let timeslot: TimeslotIndex + public let headerHash: Data32 + + public init(timeslot: TimeslotIndex, headerHash: Data32) { + self.timeslot = timeslot + self.headerHash = headerHash + } +} + private let logger = Logger(label: "Runtime") // the STF @@ -40,8 +50,23 @@ public final class Runtime { public let config: ProtocolConfigRef - public init(config: ProtocolConfigRef) { + // nil means no ancestry tracking and checking + public var ancestry: ConfigLimitedSizeArray? + + public init( + config: ProtocolConfigRef, + ancestry: ConfigLimitedSizeArray? = nil + ) { self.config = config + self.ancestry = ancestry + } + + public func updateAncestry(with block: BlockRef) { + guard var currentAncestry = ancestry else { return } + + let newItem = AncestryItem(timeslot: block.header.timeslot, headerHash: block.hash) + currentAncestry.safeAppend(newItem) + ancestry = currentAncestry } public func validateHeader(block: Validated, state: StateRef, context: ApplyContext) throws(Error) { @@ -233,6 +258,8 @@ public final class Runtime { throw .other(error) } + updateAncestry(with: block) + return StateRef(newState) } @@ -292,7 +319,7 @@ public final class Runtime { public func updateReports(block: BlockRef, state newState: inout State) async throws -> [Ed25519PublicKey] { let result = try await newState.update( - config: config, timeslot: newState.timeslot, extrinsic: block.extrinsic.reports + config: config, timeslot: newState.timeslot, extrinsic: block.extrinsic.reports, ancestry: ancestry ) newState.reports = result.newReports return result.reporters diff --git a/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift b/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift index 0c9e092e..68254750 100644 --- a/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift +++ b/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift @@ -2,6 +2,8 @@ import Foundation import Utils public protocol ServiceAccounts: Sendable { + func copy() -> ServiceAccounts + func get(serviceAccount index: ServiceIndex) async throws -> ServiceAccountDetails? func get(serviceAccount index: ServiceIndex, storageKey key: Data) async throws -> Data? func get(serviceAccount index: ServiceIndex, preimageHash hash: Data32) async throws -> Data? @@ -37,6 +39,11 @@ public class ServiceAccountsMutRef: @unchecked Sendable { changes = AccountChanges() } + public init(copying other: ServiceAccountsMutRef) { + ref = RefMut(other.ref.value.copy()) + changes = other.changes + } + public func toRef() -> ServiceAccountsRef { ServiceAccountsRef(ref.value) } diff --git a/Blockchain/Sources/Blockchain/State/State.swift b/Blockchain/Sources/Blockchain/State/State.swift index 87857124..821e45b3 100644 --- a/Blockchain/Sources/Blockchain/State/State.swift +++ b/Blockchain/Sources/Blockchain/State/State.swift @@ -16,6 +16,12 @@ public struct State: Sendable { self.layer = layer } + public init(copying other: State) { + // backend can be shared as it's read-only until end of stf + backend = other.backend + layer = StateLayer(copying: other.layer) + } + // α: The core αuthorizations pool. public var coreAuthorizationPool: StateKeys.CoreAuthorizationPoolKey.Value { get { @@ -344,6 +350,10 @@ extension State: Dummy { } extension State: ServiceAccounts { + public func copy() -> ServiceAccounts { + State(copying: self) + } + public func get(serviceAccount index: ServiceIndex) async throws -> ServiceAccountDetails? { if layer.isDeleted(serviceAccount: index) { return nil diff --git a/Blockchain/Sources/Blockchain/State/StateLayer.swift b/Blockchain/Sources/Blockchain/State/StateLayer.swift index e6f2a90b..ff977c94 100644 --- a/Blockchain/Sources/Blockchain/State/StateLayer.swift +++ b/Blockchain/Sources/Blockchain/State/StateLayer.swift @@ -45,6 +45,10 @@ public struct StateLayer: Sendable { } } + public init(copying other: StateLayer) { + changes = other.changes + } + // α: The core αuthorizations pool. public var coreAuthorizationPool: StateKeys.CoreAuthorizationPoolKey.Value { get { diff --git a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift index 7ccc42d3..3159c3db 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift @@ -797,16 +797,19 @@ public class Bless: HostCall { var alwaysAcc: [ServiceIndex: Gas]? let length = 12 * Int(regs[4]) if state.isMemoryReadable(address: regs[3], length: length) { - alwaysAcc = [:] let data = try state.readMemory(address: regs[3], length: length) for i in stride(from: 0, to: length, by: 12) { let serviceIndex = ServiceIndex(data[i ..< i + 4].decode(UInt32.self)) let gas = Gas(data[i + 4 ..< i + 12].decode(UInt64.self)) - alwaysAcc![serviceIndex] = gas + if alwaysAcc != nil { + alwaysAcc![serviceIndex] = gas + } else { + alwaysAcc = [serviceIndex: gas] + } } } - if alwaysAcc == nil, assigners == nil { + if alwaysAcc == nil || assigners == nil { throw VMInvocationsError.panic } else if x.serviceIndex != x.state.manager { state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.HUH.rawValue) @@ -845,9 +848,11 @@ public class Assign: HostCall { var authorizationQueue: [Data32]? let length = 32 * config.value.maxAuthorizationsQueueItems if state.isMemoryReadable(address: startAddr, length: length) { - authorizationQueue = [Data32]() let data = try state.readMemory(address: startAddr, length: length) for i in stride(from: 0, to: length, by: 32) { + if authorizationQueue == nil { + authorizationQueue = [Data32]() + } authorizationQueue!.append(Data32(data[i ..< i + 32])!) } } @@ -886,9 +891,11 @@ public class Designate: HostCall { var validatorQueue: [ValidatorKey]? let length = 336 * config.value.totalNumberOfValidators if state.isMemoryReadable(address: startAddr, length: length) { - validatorQueue = [ValidatorKey]() let data = try state.readMemory(address: startAddr, length: length) for i in stride(from: 0, to: length, by: 336) { + if validatorQueue == nil { + validatorQueue = [ValidatorKey]() + } try validatorQueue!.append(ValidatorKey(data: Data(data[i ..< i + 336]))) } } @@ -921,7 +928,7 @@ public class Checkpoint: HostCall { state.writeRegister(Registers.Index(raw: 7), UInt64(bitPattern: state.getGas().value)) y.serviceIndex = x.serviceIndex - y.state = x.state + y.state = x.state.copy() y.nextAccountIndex = x.nextAccountIndex y.transfers = x.transfers y.yield = x.yield diff --git a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift index ccaa7382..b043657c 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift @@ -81,7 +81,7 @@ public final class AccumulateContext: InvocationContext { return await Log(service: context.x.serviceIndex).call(config: config, state: state) default: state.consumeGas(Gas(10)) - state.writeRegister(Registers.Index(raw: 0), HostCallResultCode.WHAT.rawValue) + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHAT.rawValue) return .continued } } diff --git a/Blockchain/Sources/Blockchain/VMInvocations/Invocations/AccumulateInvocation.swift b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/AccumulateInvocation.swift index 4b776370..28f18178 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/Invocations/AccumulateInvocation.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/AccumulateInvocation.swift @@ -14,7 +14,7 @@ public func accumulate( arguments: [OperandTuple], timeslot: TimeslotIndex ) async throws -> AccumulationResult { - logger.debug("accumulating service index: \(serviceIndex)") + logger.debug("accumulating service index: \(serviceIndex), gas: \(gas)") guard let accumulatingAccountDetails = try await state.accounts.value.get(serviceAccount: serviceIndex), let preimage = try await state.accounts.value.get( diff --git a/Fuzzing/Sources/Fuzzing/FuzzingClient.swift b/Fuzzing/Sources/Fuzzing/FuzzingClient.swift index 34d7079b..bc430810 100644 --- a/Fuzzing/Sources/Fuzzing/FuzzingClient.swift +++ b/Fuzzing/Sources/Fuzzing/FuzzingClient.swift @@ -83,11 +83,18 @@ public class FuzzingClient { throw FuzzingClientError.connectionFailed } - let message = FuzzingMessage.peerInfo(.init(name: "boka-fuzzing-fuzzer")) + let message = FuzzingMessage.peerInfo(.init( + name: "boka-fuzzing-fuzzer", + fuzzFeatures: 0 + )) try connection.sendMessage(message) if let response = try connection.receiveMessage(), case let .peerInfo(info) = response { - logger.info("🤝 Handshake completed with \(info.name), app version: \(info.appVersion), jam version: \(info.jamVersion)") + logger.info("🤝 Handshake completed with \(info.appName), app version: \(info.appVersion), jam version: \(info.jamVersion)") + logger.info(" Fuzz version: \(info.fuzzVersion)") + let ancestryEnabled = (info.fuzzFeatures & FEATURE_ANCESTRY) != 0 + let forkEnabled = (info.fuzzFeatures & FEATURE_FORK) != 0 + logger.info(" Features: ANCESTRY=\(ancestryEnabled), FORK=\(forkEnabled)") } else { throw FuzzingClientError.targetNotResponding } @@ -113,7 +120,7 @@ public class FuzzingClient { currentStateRef = state.asRef() // set state on target - try await setState(kv: kv, connection: connection) + try await initializeState(kv: kv, connection: connection) // generate a block let blockRef = try await fuzzGenerator.generateBlock( @@ -122,9 +129,17 @@ public class FuzzingClient { config: config ) + // TODO: Implement fork feature + // import block on target let targetStateRoot = try importBlock(block: blockRef.value, connection: connection) + // skip state comparison if import failed + guard let targetStateRoot else { + logger.warning("⚠️ Skipping block \(blockIndex + 1) due to import failure, continuing with next block") + continue + } + // get expected post-state let (expectedStateRoot, expectedPostState) = try await fuzzGenerator.generatePostState(timeslot: timeslot, config: config) @@ -152,29 +167,38 @@ public class FuzzingClient { logger.info("🎯 Fuzzing session completed - processed \(blockCount) blocks") } - private func setState(kv: [FuzzKeyValue], connection: FuzzingSocketConnection) async throws { - logger.info("🏗️ SET STATE") + private func initializeState(kv: [FuzzKeyValue], connection: FuzzingSocketConnection) async throws { + logger.info("🏗️ INITIALIZE STATE") let dummyHeader = Header.dummy(config: config) - let setStateMessage = FuzzingMessage.setState(FuzzSetState(header: dummyHeader, state: kv)) - try connection.sendMessage(setStateMessage) + let ancestry: [AncestryItem] = [] + let initializeMessage = FuzzingMessage.initialize(FuzzInitialize(header: dummyHeader, state: kv, ancestry: ancestry)) + try connection.sendMessage(initializeMessage) if let response = try connection.receiveMessage(), case let .stateRoot(root) = response { - logger.info("🏗️ SET STATE success, root: \(root.data.toHexString())") + logger.info("🏗️ INITIALIZE STATE success, root: \(root.data.toHexString())") } else { throw FuzzingClientError.targetNotResponding } } - private func importBlock(block: Block, connection: FuzzingSocketConnection) throws -> Data32 { + private func importBlock(block: Block, connection: FuzzingSocketConnection) throws -> Data32? { logger.info("📦 IMPORT BLOCK") let importMessage = FuzzingMessage.importBlock(block) try connection.sendMessage(importMessage) - if let response = try connection.receiveMessage(), case let .stateRoot(root) = response { - logger.info("📦 IMPORT BLOCK success, new state root: \(root.data.toHexString())") - return root + if let response = try connection.receiveMessage() { + switch response { + case let .stateRoot(root): + logger.info("📦 IMPORT BLOCK success, new state root: \(root.data.toHexString())") + return root + case let .error(errorMsg): + logger.error("📦 IMPORT BLOCK failed: \(errorMsg)") + return nil + default: + throw FuzzingClientError.targetNotResponding + } } else { throw FuzzingClientError.targetNotResponding } diff --git a/Fuzzing/Sources/Fuzzing/FuzzingTarget.swift b/Fuzzing/Sources/Fuzzing/FuzzingTarget.swift index 2d53a62d..5a922230 100644 --- a/Fuzzing/Sources/Fuzzing/FuzzingTarget.swift +++ b/Fuzzing/Sources/Fuzzing/FuzzingTarget.swift @@ -15,7 +15,9 @@ public class FuzzingTarget { private let socket: FuzzingSocket private let runtime: Runtime private let config: ProtocolConfigRef - private var currentStateRef: StateRef? + private var currentStateRef: StateRef? // the latest STF result + private var baseStateRef: StateRef? // fork base state (changes on chain extensions) + private var negotiatedFeatures: UInt32 = 0 public init(socketPath: String, config: String) throws { switch config { @@ -30,6 +32,7 @@ public class FuzzingTarget { runtime = Runtime(config: self.config) currentStateRef = nil + baseStateRef = nil socket = FuzzingSocket(socketPath: socketPath, config: self.config) logger.info("Boka Fuzzing Target initialized on socket \(socketPath)") @@ -72,24 +75,47 @@ public class FuzzingTarget { case let .peerInfo(peerInfo): try await handleHandShake(peerInfo: peerInfo, connection: connection) + case let .initialize(initialize): + try await handleInitialize(initialize: initialize, connection: connection) + case let .importBlock(block): try await handleImportBlock(block: block, connection: connection) - case let .setState(setState): - try await handleSetState(setState: setState, connection: connection) - case let .getState(headerHash): try await handleGetState(headerHash: headerHash, connection: connection) - case .state, .stateRoot: - logger.warning("Received response message (ignored)") + case .state, .stateRoot, .error: + logger.warning("Received response message \(message), ignored") } } private func handleHandShake(peerInfo: FuzzPeerInfo, connection: FuzzingSocketConnection) async throws { - logger.info("Handshake from: \(peerInfo.name), App Version: \(peerInfo.appVersion), Jam Version: \(peerInfo.jamVersion)") - let message = FuzzingMessage.peerInfo(FuzzPeerInfo(name: "boka-fuzzing-target")) + logger.info("Handshake from: \(peerInfo.appName), App Version: \(peerInfo.appVersion), Jam Version: \(peerInfo.jamVersion)") + logger.info(" Fuzz version: \(peerInfo.fuzzVersion)") + + // fuzzer features + let ancestryEnabled = (peerInfo.fuzzFeatures & FEATURE_ANCESTRY) != 0 + let forkEnabled = (peerInfo.fuzzFeatures & FEATURE_FORK) != 0 + logger.info(" Fuzzer features: ANCESTRY=\(ancestryEnabled), FORK=\(forkEnabled)") + + let targetPeerInfo = FuzzPeerInfo( + name: "boka-fuzzing-target", + fuzzFeatures: FEATURE_ANCESTRY | FEATURE_FORK + ) + // our features + let ourAncestryEnabled = (targetPeerInfo.fuzzFeatures & FEATURE_ANCESTRY) != 0 + let ourForkEnabled = (targetPeerInfo.fuzzFeatures & FEATURE_FORK) != 0 + logger.info(" Our features: ANCESTRY=\(ourAncestryEnabled), FORK=\(ourForkEnabled)") + + // negotiated features (intersection of fuzzer and target features) + negotiatedFeatures = peerInfo.fuzzFeatures & targetPeerInfo.fuzzFeatures + let negotiatedAncestryEnabled = (negotiatedFeatures & FEATURE_ANCESTRY) != 0 + let negotiatedForkEnabled = (negotiatedFeatures & FEATURE_FORK) != 0 + logger.info(" Negotiated features: ANCESTRY=\(negotiatedAncestryEnabled), FORK=\(negotiatedForkEnabled)") + + let message = FuzzingMessage.peerInfo(targetPeerInfo) try connection.sendMessage(message) + logger.info("Handshake completed") } @@ -98,31 +124,70 @@ public class FuzzingTarget { logger.info("Block slot: \(block.header.timeslot)") do { - guard let stateRef = currentStateRef else { throw FuzzingTargetError.stateNotSet } + guard let currentStateRef else { throw FuzzingTargetError.stateNotSet } + + let workingStateRef: StateRef + + if (negotiatedFeatures & FEATURE_FORK) != 0 { + // has fork feature + guard let baseStateRef else { throw FuzzingTargetError.stateNotSet } + + let baseParent = baseStateRef.value.lastBlockHash + let currentParent = currentStateRef.value.lastBlockHash + + if block.header.parentHash == currentParent, currentParent != baseParent { + // extends from current - advance base to current + self.baseStateRef = currentStateRef + } + + // operate on a copy of base state + workingStateRef = try await createStateCopy(from: self.baseStateRef!) + } else { + // no fork feature + workingStateRef = currentStateRef + } - let newStateRef = try await runtime.apply(block: block.asRef(), state: stateRef) + let newStateRef = try await runtime.apply(block: block.asRef(), state: workingStateRef) - currentStateRef = newStateRef + self.currentStateRef = newStateRef logger.info("IMPORT BLOCK completed") - let stateRoot = await currentStateRef?.value.stateRoot ?? Data32() + let stateRoot = await newStateRef.value.stateRoot logger.info("State root: \(stateRoot)") let response = FuzzingMessage.stateRoot(stateRoot) try connection.sendMessage(response) } catch { logger.error("❌ Failed to import block: \(error)") - let stateRoot = await currentStateRef?.value.stateRoot ?? Data32() - let response = FuzzingMessage.stateRoot(stateRoot) + let errorMsg = "Chain error: block import failure: \(error)" + let response = FuzzingMessage.error(errorMsg) try connection.sendMessage(response) } } - private func handleSetState(setState: FuzzSetState, connection: FuzzingSocketConnection) async throws { - logger.info("SET STATE: \(setState.state.count) key-value pairs") + private func createStateCopy(from stateRef: StateRef) async throws -> StateRef { + let keyValuePairs = try await stateRef.value.backend.getKeys(nil, nil, nil) + let newBackend = StateBackend(InMemoryBackend(), config: config, rootHash: Data32()) + try await newBackend.writeRaw(keyValuePairs.map { (key: Data31($0.key)!, value: $0.value as Data?) }) + let newState = try await State(backend: newBackend) + return newState.asRef() + } + + private func handleInitialize(initialize: FuzzInitialize, connection: FuzzingSocketConnection) async throws { + logger.info("INITIALIZE STATE: \(initialize.state.count) key-value pairs") + logger.info(" Header: \(initialize.header.hash())") + logger.info(" Ancestry: \(initialize.ancestry.count) items") do { + if (negotiatedFeatures & FEATURE_ANCESTRY) != 0 { + runtime.ancestry = try .init(config: config, array: initialize.ancestry) + logger.info(" Ancestry feature enabled, set \(initialize.ancestry.count) ancestry items") + } else { + runtime.ancestry = nil + logger.info(" Ancestry feature disabled") + } + // set state - let rawKV = setState.state.map { (key: $0.key, value: $0.value) } + let rawKV = initialize.state.map { (key: $0.key, value: $0.value) } let backend = StateBackend(InMemoryBackend(), config: config, rootHash: Data32()) try await backend.writeRaw(rawKV) let state = try await State(backend: backend) @@ -134,14 +199,19 @@ public class FuzzingTarget { currentStateRef = stateRef - logger.info("SET STATE completed") + // create base state copy if fork feature is enabled + if (negotiatedFeatures & FEATURE_FORK) != 0 { + baseStateRef = try await createStateCopy(from: stateRef) + } else { + baseStateRef = nil + } + + logger.info("INITIALIZE STATE completed") let response = FuzzingMessage.stateRoot(root) try connection.sendMessage(response) } catch { - logger.error("❌ Failed to set state: \(error)") - let stateRoot = await currentStateRef?.value.stateRoot ?? Data32() - let response = FuzzingMessage.stateRoot(stateRoot) - try connection.sendMessage(response) + logger.error("❌ Failed to initialize state: \(error)") + throw error } } diff --git a/Fuzzing/Sources/Fuzzing/Messages.swift b/Fuzzing/Sources/Fuzzing/Messages.swift index febe014d..041aa5c0 100644 --- a/Fuzzing/Sources/Fuzzing/Messages.swift +++ b/Fuzzing/Sources/Fuzzing/Messages.swift @@ -25,19 +25,28 @@ extension FuzzVersion: CustomStringConvertible { } } +public let FEATURE_ANCESTRY: UInt32 = 1 +public let FEATURE_FORK: UInt32 = 2 + public struct FuzzPeerInfo: Codable { - public let name: String - public let appVersion: FuzzVersion + public let fuzzVersion: UInt8 + public let fuzzFeatures: UInt32 public let jamVersion: FuzzVersion + public let appVersion: FuzzVersion + public let appName: String public init( name: String, appVersion: FuzzVersion = FuzzVersion(major: 0, minor: 1, patch: 0), - jamVersion: FuzzVersion = FuzzVersion(major: 0, minor: 7, patch: 0) + jamVersion: FuzzVersion = FuzzVersion(major: 0, minor: 7, patch: 0), + fuzzVersion: UInt8 = 1, + fuzzFeatures: UInt32 = FEATURE_ANCESTRY | FEATURE_FORK ) { - self.name = name + appName = name self.appVersion = appVersion self.jamVersion = jamVersion + self.fuzzVersion = fuzzVersion + self.fuzzFeatures = fuzzFeatures } } @@ -53,13 +62,15 @@ public struct FuzzKeyValue: Codable { public typealias FuzzState = [FuzzKeyValue] -public struct FuzzSetState: Codable { +public struct FuzzInitialize: Codable { public let header: Header public let state: FuzzState + public let ancestry: [AncestryItem] - public init(header: Header, state: FuzzState) { + public init(header: Header, state: FuzzState, ancestry: [AncestryItem]) { self.header = header self.state = state + self.ancestry = ancestry } } @@ -68,11 +79,12 @@ public typealias FuzzStateRoot = Data32 // StateRootHash public enum FuzzingMessage: Codable { case peerInfo(FuzzPeerInfo) + case initialize(FuzzInitialize) + case stateRoot(FuzzStateRoot) case importBlock(Block) - case setState(FuzzSetState) case getState(FuzzGetState) case state(FuzzState) - case stateRoot(FuzzStateRoot) + case error(String) public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() @@ -81,15 +93,17 @@ public enum FuzzingMessage: Codable { case 0: self = try .peerInfo(container.decode(FuzzPeerInfo.self)) case 1: - self = try .importBlock(container.decode(Block.self)) + self = try .initialize(container.decode(FuzzInitialize.self)) case 2: - self = try .setState(container.decode(FuzzSetState.self)) + self = try .stateRoot(container.decode(FuzzStateRoot.self)) case 3: - self = try .getState(container.decode(FuzzGetState.self)) + self = try .importBlock(container.decode(Block.self)) case 4: - self = try .state(container.decode(FuzzState.self)) + self = try .getState(container.decode(FuzzGetState.self)) case 5: - self = try .stateRoot(container.decode(FuzzStateRoot.self)) + self = try .state(container.decode(FuzzState.self)) + case 255: + self = try .error(container.decode(String.self)) default: throw DecodingError.dataCorrupted( DecodingError.Context( @@ -106,21 +120,24 @@ public enum FuzzingMessage: Codable { case let .peerInfo(value): try container.encode(UInt8(0)) try container.encode(value) - case let .importBlock(value): + case let .initialize(value): try container.encode(UInt8(1)) try container.encode(value) - case let .setState(value): + case let .stateRoot(value): try container.encode(UInt8(2)) try container.encode(value) - case let .getState(value): + case let .importBlock(value): try container.encode(UInt8(3)) try container.encode(value) - case let .state(value): + case let .getState(value): try container.encode(UInt8(4)) try container.encode(value) - case let .stateRoot(value): + case let .state(value): try container.encode(UInt8(5)) try container.encode(value) + case let .error(value): + try container.encode(UInt8(255)) + try container.encode(value) } } } diff --git a/JAMTests/Benchmarks/TestVectors/TestVectors.swift b/JAMTests/Benchmarks/TestVectors/TestVectors.swift index 6f07aea0..88bc7fe5 100644 --- a/JAMTests/Benchmarks/TestVectors/TestVectors.swift +++ b/JAMTests/Benchmarks/TestVectors/TestVectors.swift @@ -38,36 +38,42 @@ let benchmarks: @Sendable () -> Void = { let tests = try? JSONDecoder().decode([ShuffleTestCase].self, from: data), !tests.isEmpty { - Benchmark("w3f.shuffle") { _ in - for test in tests { - var input = Array(0 ..< test.input) - if let entropy = Data32(fromHexString: test.entropy) { - input.shuffle(randomness: entropy) - blackHole(input) + Benchmark("w3f.shuffle", configuration: .init(timeUnits: .microseconds)) { _ in + for _ in 0 ..< 10 { // 10 iterations + for test in tests { + var input = Array(0 ..< test.input) + if let entropy = Data32(fromHexString: test.entropy) { + input.shuffle(randomness: entropy) + blackHole(input) + } } } } } // Traces - let tracePaths = ["traces/fallback", "traces/safrole", "traces/storage", "traces/preimages"] - for path in tracePaths { + let tracePaths = [("traces/fallback", 15), ("traces/safrole", 10), ("traces/storage", 5), ("traces/preimages", 5)] + for (path, iterations) in tracePaths { let traces = try! JamTestnet.loadTests(path: path, src: .w3f) - Benchmark("w3f.traces.\(path.components(separatedBy: "/").last!)") { benchmark in - for trace in traces { - let testcase = try! JamTestnet.decodeTestcase(trace) - benchmark.startMeasurement() - let result = try? await JamTestnet.runSTF(testcase) - switch result { - case let .success(stateRef): - let root = await stateRef.value.stateRoot - blackHole(root) - case .failure: - blackHole(trace.description) - case .none: - blackHole(trace.description) + Benchmark( + "w3f.traces.\(path.components(separatedBy: "/").last!)", + ) { benchmark in + for _ in 0 ..< iterations { + for trace in traces { + let testcase = try! JamTestnet.decodeTestcase(trace) + benchmark.startMeasurement() + let result = try? await JamTestnet.runSTF(testcase) + switch result { + case let .success(stateRef): + let root = await stateRef.value.stateRoot + blackHole(root) + case .failure: + blackHole(trace.description) + case .none: + blackHole(trace.description) + } + benchmark.stopMeasurement() } - benchmark.stopMeasurement() } } } diff --git a/JAMTests/Sources/JAMTests/JamTestnet.swift b/JAMTests/Sources/JAMTests/JamTestnet.swift index 3fd03c10..78ec4457 100644 --- a/JAMTests/Sources/JAMTests/JamTestnet.swift +++ b/JAMTests/Sources/JAMTests/JamTestnet.swift @@ -68,7 +68,7 @@ public enum JamTestnet { _ testcase: JamTestnetTestcase, config: ProtocolConfigRef = TestVariants.tiny.config ) async throws -> Result { - let runtime = Runtime(config: config) + let runtime = Runtime(config: config, ancestry: nil) let blockRef = testcase.block.asRef() let stateRef = try await testcase.preState.toState(config: config).asRef() diff --git a/JAMTests/Tests/JAMTests/jamtestnet/FuzzTests.swift b/JAMTests/Tests/JAMTests/jamtestnet/FuzzTests.swift index b79ca48f..e8ca54e5 100644 --- a/JAMTests/Tests/JAMTests/jamtestnet/FuzzTests.swift +++ b/JAMTests/Tests/JAMTests/jamtestnet/FuzzTests.swift @@ -40,15 +40,6 @@ struct FuzzTests { ], ignore: [ // traces - ("0.7.0/1756548583", "00000009"), // TODO: find root cause - ("0.7.0/1757406079", "00000011"), // TODO: one extra key - ("0.7.0/1757406516", "00000022"), // TODO: one storage mismatch - ("0.7.0/1757406558", "00000031"), // TODO: one storage mismatch - ("0.7.0/1757406558", "00000032"), // TODO: many - ("0.7.0/1757421101", "00000091"), // TODO: many - ("0.7.0/1757421824", "00000020"), // TODO: one storage mismatch - ("0.7.0/1757421824", "00000021"), // TODO: one storage mismatch - ("0.7.0/1757422206", "00000011"), // TODO: one storage mismatch ] )) func v070(_ input: Testcase) async throws { diff --git a/JAMTests/Tests/JAMTests/jamtestnet/TraceTest.swift b/JAMTests/Tests/JAMTests/jamtestnet/TraceTest.swift index c9fc8399..ed7bf566 100644 --- a/JAMTests/Tests/JAMTests/jamtestnet/TraceTest.swift +++ b/JAMTests/Tests/JAMTests/jamtestnet/TraceTest.swift @@ -61,7 +61,7 @@ enum TraceTest { let data31 = Data31(key)! #expect( expectedStateDict[data31] != nil, - "extra key in boka post state: \(data31.toHexString()), value len: \(value.count)" + "extra key in boka post state: \(data31.toHexString()), value: \(value.toDebugHexString())" ) } diff --git a/JAMTests/Tests/JAMTests/w3f/AccumulateTests.swift b/JAMTests/Tests/JAMTests/w3f/AccumulateTests.swift index 8ec14a3a..7bd7c5f8 100644 --- a/JAMTests/Tests/JAMTests/w3f/AccumulateTests.swift +++ b/JAMTests/Tests/JAMTests/w3f/AccumulateTests.swift @@ -108,6 +108,10 @@ private struct FullAccumulateState: Accumulation { var preimages: [ServiceIndex: [Data32: Data]] = [:] var preimageInfo: [ServiceIndex: [Data32: StateKeys.ServiceAccountPreimageInfoKey.Value]] = [:] + func copy() -> ServiceAccounts { + self + } + func get(serviceAccount index: ServiceIndex) async throws -> ServiceAccountDetails? { accounts[index] } diff --git a/JAMTests/Tests/JAMTests/w3f/ReportsTests.swift b/JAMTests/Tests/JAMTests/w3f/ReportsTests.swift index 3e7baf29..1109bf48 100644 --- a/JAMTests/Tests/JAMTests/w3f/ReportsTests.swift +++ b/JAMTests/Tests/JAMTests/w3f/ReportsTests.swift @@ -103,7 +103,8 @@ struct ReportsTests { return try await state.update( config: config, timeslot: testcase.input.timeslot, - extrinsic: testcase.input.reports + extrinsic: testcase.input.reports, + ancestry: nil ) } switch result { diff --git a/Node/Tests/NodeTests/NodeTests.swift b/Node/Tests/NodeTests/NodeTests.swift index 0be46fba..895df73a 100644 --- a/Node/Tests/NodeTests/NodeTests.swift +++ b/Node/Tests/NodeTests/NodeTests.swift @@ -114,7 +114,7 @@ final class NodeTests { await nodeStoreMiddlware.wait() // Add a small delay to ensure block production completes before next advance - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(150)) } // Wait for sync