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
2 changes: 1 addition & 1 deletion Blockchain/Sources/Blockchain/Blockchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ extension Guaranteeing {
public func update(
config: ProtocolConfigRef,
timeslot: TimeslotIndex,
extrinsic: ExtrinsicGuarantees
extrinsic: ExtrinsicGuarantees,
ancestry: ConfigLimitedSizeArray<AncestryItem, ProtocolConfig.Int0, ProtocolConfig.MaxLookupAnchorAge>?
) async throws(GuaranteeingError) -> (
newReports: ConfigFixedSizeArray<
ReportItem?,
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 29 additions & 2 deletions Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<AncestryItem, ProtocolConfig.Int0, ProtocolConfig.MaxLookupAnchorAge>?

public init(
config: ProtocolConfigRef,
ancestry: ConfigLimitedSizeArray<AncestryItem, ProtocolConfig.Int0, ProtocolConfig.MaxLookupAnchorAge>? = 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<BlockRef>, state: StateRef, context: ApplyContext) throws(Error) {
Expand Down Expand Up @@ -233,6 +258,8 @@ public final class Runtime {
throw .other(error)
}

updateAncestry(with: block)

return StateRef(newState)
}

Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Blockchain/Sources/Blockchain/State/ServiceAccounts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
}
Expand Down
10 changes: 10 additions & 0 deletions Blockchain/Sources/Blockchain/State/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Blockchain/Sources/Blockchain/State/StateLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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])!)
}
}
Expand Down Expand Up @@ -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])))
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
48 changes: 36 additions & 12 deletions Fuzzing/Sources/Fuzzing/FuzzingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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
}
Expand Down
Loading