diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift index 25f5c429..cc64c7f4 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift @@ -32,7 +32,8 @@ extension Ref where T == ProtocolConfig { erasureCodedPieceSize: 684, maxWorkPackageManifestEntries: 1 << 11, maxEncodedWorkPackageSize: 12 * 1 << 20, - maxEncodedWorkReportSize: 96 * 1 << 10, + segmentSize: 4104, + maxWorkReportOutputSize: 96 * 1 << 10, erasureCodedSegmentSize: 6, ticketSubmissionEndSlot: 2, pvmDynamicAddressAlignmentFactor: 2, @@ -72,7 +73,8 @@ extension Ref where T == ProtocolConfig { erasureCodedPieceSize: 684, maxWorkPackageManifestEntries: 1 << 11, maxEncodedWorkPackageSize: 12 * 1 << 20, - maxEncodedWorkReportSize: 96 * 1 << 10, + segmentSize: 4104, + maxWorkReportOutputSize: 96 * 1 << 10, erasureCodedSegmentSize: 6, ticketSubmissionEndSlot: 10, pvmDynamicAddressAlignmentFactor: 2, @@ -111,7 +113,8 @@ extension Ref where T == ProtocolConfig { erasureCodedPieceSize: 684, maxWorkPackageManifestEntries: 1 << 11, maxEncodedWorkPackageSize: 12 * 1 << 20, - maxEncodedWorkReportSize: 96 * 1 << 10, + segmentSize: 4104, + maxWorkReportOutputSize: 96 * 1 << 10, erasureCodedSegmentSize: 6, ticketSubmissionEndSlot: 500, pvmDynamicAddressAlignmentFactor: 2, diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift index 65435718..3f8f8a34 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift @@ -85,14 +85,17 @@ public struct ProtocolConfig: Sendable, Codable, Equatable { // WM = 2^11: The maximum number of entries in a work-package manifest. public var maxWorkPackageManifestEntries: Int - // WP = 12 * 2^20: The maximum size of an encoded work-package together with its extrinsic data and import impli- + // WB = 12 * 2^20: The maximum size of an encoded work-package together with its extrinsic data and import impli- // cations, in octets. public var maxEncodedWorkPackageSize: Int - // WR = 96 * 2^10: The maximum size of an encoded work-report in octets. - public var maxEncodedWorkReportSize: Int + // WG = WP*WE = 4104: The size of a segment in octets. + public var segmentSize: Int - // WS = 6: The size of an exported segment in erasure-coded pieces. + // WR = 48 * 2^10: The maximum total size of all output blobs in a work-report, in octets. + public var maxWorkReportOutputSize: Int + + // WP = 6: The number of erasure-coded pieces in a segment. public var erasureCodedSegmentSize: Int // WT = 128: The size of a transfer memo in octets. @@ -143,7 +146,8 @@ public struct ProtocolConfig: Sendable, Codable, Equatable { erasureCodedPieceSize: Int, maxWorkPackageManifestEntries: Int, maxEncodedWorkPackageSize: Int, - maxEncodedWorkReportSize: Int, + segmentSize: Int, + maxWorkReportOutputSize: Int, erasureCodedSegmentSize: Int, ticketSubmissionEndSlot: Int, pvmDynamicAddressAlignmentFactor: Int, @@ -180,7 +184,8 @@ public struct ProtocolConfig: Sendable, Codable, Equatable { self.erasureCodedPieceSize = erasureCodedPieceSize self.maxWorkPackageManifestEntries = maxWorkPackageManifestEntries self.maxEncodedWorkPackageSize = maxEncodedWorkPackageSize - self.maxEncodedWorkReportSize = maxEncodedWorkReportSize + self.segmentSize = segmentSize + self.maxWorkReportOutputSize = maxWorkReportOutputSize self.erasureCodedSegmentSize = erasureCodedSegmentSize self.ticketSubmissionEndSlot = ticketSubmissionEndSlot self.pvmDynamicAddressAlignmentFactor = pvmDynamicAddressAlignmentFactor @@ -260,8 +265,9 @@ extension ProtocolConfig { ? other.maxWorkPackageManifestEntries : maxWorkPackageManifestEntries, maxEncodedWorkPackageSize: other.maxEncodedWorkPackageSize != 0 ? other.maxEncodedWorkPackageSize : maxEncodedWorkPackageSize, - maxEncodedWorkReportSize: other.maxEncodedWorkReportSize != 0 - ? other.maxEncodedWorkReportSize : maxEncodedWorkReportSize, + segmentSize: other.segmentSize != 0 ? other.segmentSize : segmentSize, + maxWorkReportOutputSize: other.maxWorkReportOutputSize != 0 + ? other.maxWorkReportOutputSize : maxWorkReportOutputSize, erasureCodedSegmentSize: other.erasureCodedSegmentSize != 0 ? other.erasureCodedSegmentSize : erasureCodedSegmentSize, ticketSubmissionEndSlot: other.ticketSubmissionEndSlot != 0 @@ -347,8 +353,9 @@ extension ProtocolConfig { maxEncodedWorkPackageSize = try decode( .maxEncodedWorkPackageSize, defaultValue: 0, required: required ) - maxEncodedWorkReportSize = try decode( - .maxEncodedWorkReportSize, defaultValue: 0, required: required + segmentSize = try decode(.segmentSize, defaultValue: 0, required: required) + maxWorkReportOutputSize = try decode( + .maxWorkReportOutputSize, defaultValue: 0, required: required ) erasureCodedSegmentSize = try decode( .erasureCodedSegmentSize, defaultValue: 0, required: required @@ -579,10 +586,17 @@ extension ProtocolConfig { } } - public enum MaxEncodedWorkReportSize: ReadInt { + public enum SegmentSize: ReadInt { + public typealias TConfig = ProtocolConfigRef + public static func read(config: ProtocolConfigRef) -> Int { + config.value.segmentSize + } + } + + public enum MaxWorkReportOutputSize: ReadInt { public typealias TConfig = ProtocolConfigRef public static func read(config: ProtocolConfigRef) -> Int { - config.value.maxEncodedWorkReportSize + config.value.maxWorkReportOutputSize } } diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift index 6cf55f9b..31bac061 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift @@ -56,6 +56,7 @@ public struct SingleAccumulationOutput { } public protocol Accumulation: ServiceAccounts { + var timeslot: TimeslotIndex { get } var privilegedServices: PrivilegedServices { get } var validatorQueue: ConfigFixedSizeArray< ValidatorKey, ProtocolConfig.TotalNumberOfValidators @@ -359,6 +360,7 @@ extension Accumulation { config: config, service: service, serviceAccounts: &self, + timeslot: timeslot, transfers: transfers ) } diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/OnTransferFunction.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/OnTransferFunction.swift index 675b982c..317b2abb 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/OnTransferFunction.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/OnTransferFunction.swift @@ -5,6 +5,7 @@ public protocol OnTransferFunction { config: ProtocolConfigRef, service: ServiceIndex, serviceAccounts: inout some ServiceAccounts, + timeslot: TimeslotIndex, transfers: [DeferredTransfers] ) async throws } diff --git a/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift b/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift index 624e4323..5b3f5a70 100644 --- a/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift +++ b/Blockchain/Sources/Blockchain/State/ServiceAccounts.swift @@ -9,6 +9,8 @@ public protocol ServiceAccounts { serviceAccount index: ServiceIndex, preimageHash hash: Data32, length: UInt32 ) async throws -> StateKeys.ServiceAccountPreimageInfoKey.Value? + func historicalLookup(serviceAccount index: ServiceIndex, timeslot: TimeslotIndex, preimageHash hash: Data32) async throws -> Data? + mutating func set(serviceAccount index: ServiceIndex, account: ServiceAccountDetails?) mutating func set(serviceAccount index: ServiceIndex, storageKey key: Data32, value: Data?) mutating func set(serviceAccount index: ServiceIndex, preimageHash hash: Data32, value: Data?) diff --git a/Blockchain/Sources/Blockchain/State/State.swift b/Blockchain/Sources/Blockchain/State/State.swift index 32e9a8c7..d3d69ccc 100644 --- a/Blockchain/Sources/Blockchain/State/State.swift +++ b/Blockchain/Sources/Blockchain/State/State.swift @@ -362,6 +362,29 @@ extension State: ServiceAccounts { return try await backend.read(StateKeys.ServiceAccountPreimageInfoKey(index: index, hash: hash, length: length)) } + public func historicalLookup( + serviceAccount index: ServiceIndex, + timeslot: TimeslotIndex, + preimageHash hash: Data32 + ) async throws -> Data? { + if let preimage = try await get(serviceAccount: index, preimageHash: hash), + let preimageInfo = try await get(serviceAccount: index, preimageHash: hash, length: UInt32(preimage.count)) + { + var isAvailable = false + if preimageInfo.count == 1 { + isAvailable = preimageInfo[0] <= timeslot + } else if preimageInfo.count == 2 { + isAvailable = preimageInfo[0] <= timeslot && timeslot < preimageInfo[1] + } else if preimageInfo.count == 3 { + isAvailable = preimageInfo[0] <= timeslot && timeslot < preimageInfo[1] && preimageInfo[2] <= timeslot + } + + return isAvailable ? preimage : nil + } else { + return nil + } + } + public mutating func set(serviceAccount index: ServiceIndex, account: ServiceAccountDetails?) { layer[serviceAccount: index] = account } @@ -456,6 +479,7 @@ struct DummyFunction: AccumulateFunction, OnTransferFunction { config _: ProtocolConfigRef, service _: ServiceIndex, serviceAccounts _: inout some ServiceAccounts, + timeslot _: TimeslotIndex, transfers _: [DeferredTransfers] ) async throws { fatalError("not implemented") diff --git a/Blockchain/Sources/Blockchain/Types/WorkReport.swift b/Blockchain/Sources/Blockchain/Types/WorkReport.swift index 0b5d6054..f3d83da6 100644 --- a/Blockchain/Sources/Blockchain/Types/WorkReport.swift +++ b/Blockchain/Sources/Blockchain/Types/WorkReport.swift @@ -90,7 +90,8 @@ extension WorkReport: Validate { guard refinementContext.prerequisiteWorkPackages.count + lookup.count <= config.value.maxDepsInWorkReport else { throw .tooManyDependencies } - guard encodedSize <= config.value.maxEncodedWorkReportSize else { + let resultOutputSize = results.compactMap { result in try? result.output.result.get() }.reduce(0) { $0 + $1.count } + guard authorizationOutput.count + resultOutputSize <= config.value.maxWorkReportOutputSize else { throw .tooBig } guard coreIndex < UInt32(config.value.totalNumberOfCores) else { diff --git a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift index e108912c..8d1170c9 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift @@ -239,14 +239,16 @@ public class Bless: HostCall { } } - if basicGas.count != 0 { + if basicGas.count == 0 { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else if !regs[0 ..< 4].allSatisfy({ $0 >= 0 && $0 <= Int(UInt32.max) }) { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + } else { state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) x.accumulateState.privilegedServices.blessed = regs[0] x.accumulateState.privilegedServices.assign = regs[1] x.accumulateState.privilegedServices.designate = regs[2] x.accumulateState.privilegedServices.basicGas = basicGas - } else { - state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) } } } @@ -273,13 +275,13 @@ public class Assign: HostCall { } } - if targetCoreIndex < config.value.totalNumberOfCores, !authorizationQueue.isEmpty { - x.accumulateState.authorizationQueue[targetCoreIndex] = try ConfigFixedSizeArray(config: config, array: authorizationQueue) - state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) - } else if authorizationQueue.isEmpty { + if authorizationQueue.isEmpty { state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) - } else { + } else if targetCoreIndex > config.value.totalNumberOfCores { state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.CORE.rawValue) + } else { + x.accumulateState.authorizationQueue[targetCoreIndex] = try ConfigFixedSizeArray(config: config, array: authorizationQueue) + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) } } } @@ -306,11 +308,11 @@ public class Designate: HostCall { } } - if !validatorQueue.isEmpty { + if validatorQueue.isEmpty { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else { x.accumulateState.validatorQueue = try ConfigFixedSizeArray(config: config, array: validatorQueue) state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) - } else { - state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) } } } @@ -609,3 +611,378 @@ public class Forget: HostCall { } } } + +/// Historical lookup +public class HistoricalLookup: HostCall { + public static var identifier: UInt8 { 15 } + + public let context: RefineContext.ContextType + public let service: ServiceIndex + public let serviceAccounts: ServiceAccounts + public let lookupAnchorTimeslot: TimeslotIndex + + public init( + context: RefineContext.ContextType, + service: ServiceIndex, + serviceAccounts: ServiceAccounts, + lookupAnchorTimeslot: TimeslotIndex + ) { + self.context = context + self.lookupAnchorTimeslot = lookupAnchorTimeslot + self.service = service + self.serviceAccounts = serviceAccounts + } + + public func _callImpl(config _: ProtocolConfigRef, state: VMState) async throws { + var serviceIndex: ServiceIndex? + let reg7: UInt64 = state.readRegister(Registers.Index(raw: 7)) + if reg7 == Int64.max, try await serviceAccounts.get(serviceAccount: service) != nil { + serviceIndex = service + } else if try await serviceAccounts.get(serviceAccount: UInt32(truncatingIfNeeded: reg7)) != nil { + serviceIndex = UInt32(truncatingIfNeeded: reg7) + } + + let regs: [UInt32] = state.readRegisters(in: 8 ..< 11) + + let isReadable = state.isMemoryReadable(address: regs[0], length: 32) + let preimageHash = isReadable ? try? Blake2b256.hash(state.readMemory(address: regs[0], length: 32)) : nil + let isWritable = state.isMemoryWritable(address: regs[1], length: Int(regs[2])) + + var preimage: Data? + if let preimageHash, let serviceIndex, isWritable { + preimage = try await serviceAccounts.historicalLookup( + serviceAccount: serviceIndex, + timeslot: lookupAnchorTimeslot, + preimageHash: preimageHash + ) + + try state.writeMemory(address: regs[1], values: preimage ?? Data()) + } + + if preimageHash == nil || !isWritable { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else if preimage == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.NONE.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), preimage!.count) + } + } +} + +/// Import a segment to memory +public class Import: HostCall { + public static var identifier: UInt8 { 16 } + + public let context: RefineContext.ContextType + public let importSegments: [Data] + + public init(context: RefineContext.ContextType, importSegments: [Data]) { + self.context = context + self.importSegments = importSegments + } + + public func _callImpl(config: ProtocolConfigRef, state: VMState) async throws { + let reg7: UInt64 = state.readRegister(Registers.Index(raw: 7)) + let segment = reg7 < UInt64(importSegments.count) ? importSegments[Int(reg7)] : nil + + let startAddr: UInt32 = state.readRegister(Registers.Index(raw: 8)) + let length = min( + state.readRegister(Registers.Index(raw: 9)), + UInt64(config.value.segmentSize) + ) + let isWritable = state.isMemoryWritable(address: startAddr, length: Int(length)) + + if let segment, isWritable { + try state.writeMemory(address: startAddr, values: segment) + } + + if !isWritable { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else if segment == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.NONE.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) + } + } +} + +/// Export a segment from memory +public class Export: HostCall { + public static var identifier: UInt8 { 17 } + + public var context: RefineContext.ContextType + public let exportSegmentOffset: UInt64 + + public init(context: inout RefineContext.ContextType, exportSegmentOffset: UInt64) { + self.context = context + self.exportSegmentOffset = exportSegmentOffset + } + + public func _callImpl(config: ProtocolConfigRef, state: VMState) async throws { + let startAddr: UInt32 = state.readRegister(Registers.Index(raw: 7)) + let segmentSize = UInt64(config.value.segmentSize) + let length = min(state.readRegister(Registers.Index(raw: 8)), segmentSize) + let isReadable = state.isMemoryReadable(address: startAddr, length: Int(length)) + + var segment: Data? + if isReadable { + var data = try state.readMemory(address: startAddr, length: Int(length)) + let remainder = data.count % Int(segmentSize) + if remainder != 0 { + data.append(Data(repeating: 0, count: Int(segmentSize) - remainder)) + } + segment = data + } + + if segment == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else if exportSegmentOffset + UInt64(segment!.count) >= UInt64(config.value.maxWorkPackageManifestEntries) { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.FULL.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), exportSegmentOffset + UInt64(segment!.count)) + context.exports.append(segment!) + } + } +} + +/// Create an inner PVM +public class Machine: HostCall { + public static var identifier: UInt8 { 18 } + + public var context: RefineContext.ContextType + + public init(context: inout RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config _: ProtocolConfigRef, state: VMState) async throws { + let regs: [UInt32] = state.readRegisters(in: 7 ..< 10) + + let isReadable = state.isMemoryReadable(address: regs[0], length: Int(regs[1])) + + let innerVmIndex = UInt64(context.pvms.count) + let code = isReadable ? try state.readMemory(address: regs[0], length: Int(regs[1])) : nil + let pc = UInt32(truncatingIfNeeded: regs[2]) + let mem = Memory(pageMap: [], chunks: []) + + if code == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), innerVmIndex) + context.pvms[innerVmIndex] = InnerPvm(code: code!, memory: mem, pc: pc) + } + } +} + +/// Peek (read inner memory into outer memory) +public class Peek: HostCall { + public static var identifier: UInt8 { 19 } + + public let context: RefineContext.ContextType + + public init(context: RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config _: ProtocolConfigRef, state: VMState) async throws { + let regs: [UInt64] = state.readRegisters(in: 7 ..< 11) + + var segment: Data? + if context.pvms[regs[0]] == nil { + segment = Data() + } else if state.isMemoryWritable(address: regs[1], length: Int(regs[3])), context.pvms[regs[0]]!.memory.isReadable( + address: UInt32(truncatingIfNeeded: regs[2]), + length: Int(regs[3]) + ) { + segment = try context.pvms[regs[0]]!.memory.read(address: UInt32(truncatingIfNeeded: regs[2]), length: Int(regs[3])) + } + + if segment == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else if segment!.count == 0 { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) + try state.writeMemory(address: regs[1], values: segment!) + } + } +} + +/// Poke (write outer memory into inner memory) +public class Poke: HostCall { + public static var identifier: UInt8 { 20 } + + public var context: RefineContext.ContextType + + public init(context: inout RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config _: ProtocolConfigRef, state: VMState) async throws { + let regs: [UInt64] = state.readRegisters(in: 7 ..< 11) + + var segment: Data? + if context.pvms[regs[0]] == nil { + segment = Data() + } else if state.isMemoryReadable(address: regs[1], length: Int(regs[3])), context.pvms[regs[0]]!.memory.isWritable( + address: UInt32(truncatingIfNeeded: regs[2]), + length: Int(regs[3]) + ) { + segment = try state.readMemory(address: regs[1], length: Int(regs[3])) + } + + if segment == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else if segment!.count == 0 { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) + try context.pvms[regs[0]]!.memory.write(address: UInt32(truncatingIfNeeded: regs[2]), values: segment!) + } + } +} + +/// Make some pages zero and writable in the inner PVM +public class Zero: HostCall { + public static var identifier: UInt8 { 21 } + + public var context: RefineContext.ContextType + + public init(context: inout RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config: ProtocolConfigRef, state: VMState) async throws { + let regs: [UInt64] = state.readRegisters(in: 7 ..< 10) + + if context.pvms[regs[0]] == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + return + } + + if regs[1] < 16 || (regs[1] + regs[2]) >= (1 << 20) { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) + // TODO: update after pvm memory is updated to have pages and able to allocate with accessiblity + try context.pvms[regs[0]]!.memory.write( + address: UInt32(truncatingIfNeeded: regs[1] * UInt64(config.value.pvmMemoryPageSize)), + values: Data(repeating: 0, count: Int(regs[2] * UInt64(config.value.pvmMemoryPageSize))) + ) + } + } +} + +/// Make some pages zero and inaccessible in the inner PVM +public class VoidFn: HostCall { + public static var identifier: UInt8 { 22 } + + public var context: RefineContext.ContextType + + public init(context: inout RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config: ProtocolConfigRef, state: VMState) async throws { + let regs: [UInt64] = state.readRegisters(in: 7 ..< 10) + + if context.pvms[regs[0]] == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + return + } + + // TODO: update when can check if pages are inaccessible + if (regs[1] + regs[2]) >= (1 << 32) { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + } else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OK.rawValue) + // TODO: update after pvm memory is updated to have pages and able to allocate with accessiblity + try context.pvms[regs[0]]!.memory.write( + address: UInt32(truncatingIfNeeded: regs[1] * UInt64(config.value.pvmMemoryPageSize)), + values: Data(repeating: 0, count: Int(regs[2] * UInt64(config.value.pvmMemoryPageSize))) + ) + } + } +} + +/// Invoke an inner PVM +public class Invoke: HostCall { + public static var identifier: UInt8 { 23 } + + public var context: RefineContext.ContextType + + public init(context: inout RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config: ProtocolConfigRef, state: VMState) async throws { + let pvmIndex: UInt64 = state.readRegister(Registers.Index(raw: 7)) + let startAddr: UInt32 = state.readRegister(Registers.Index(raw: 8)) + + var gas: UInt64? + var registers: [UInt64] = [] + if state.isMemoryReadable(address: startAddr, length: 112) { + gas = try state.readMemory(address: startAddr, length: 8).decode(UInt64.self) + for i in 0 ..< 13 { + try registers.append(state.readMemory(address: startAddr + 8 + 8 * UInt32(i), length: 8).decode(UInt64.self)) + } + } + + guard let gas else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.OOB.rawValue) + return + } + + guard let innerPvm = context.pvms[pvmIndex] else { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + return + } + + let program = try ProgramCode(innerPvm.code) + let vm = VMState(program: program, pc: innerPvm.pc, registers: Registers(registers), gas: Gas(gas), memory: innerPvm.memory) + let engine = Engine(config: DefaultPvmConfig()) + let exitReason = await engine.execute(program: program, state: vm) + + try state.writeMemory(address: startAddr, values: JamEncoder.encode(vm.getGas()) + JamEncoder.encode(vm.getRegisters())) + context.pvms[pvmIndex]?.memory = vm.getMemoryUnsafe() + + switch exitReason { + case let .hostCall(callIndex): + context.pvms[pvmIndex]?.pc = innerPvm.pc + 1 + state.writeRegister(Registers.Index(raw: 7), HostCallResultCodeInner.HOST.rawValue) + state.writeRegister(Registers.Index(raw: 8), callIndex) + case let .pageFault(addr): + state.writeRegister(Registers.Index(raw: 7), HostCallResultCodeInner.FAULT.rawValue) + state.writeRegister(Registers.Index(raw: 8), addr) + case .outOfGas: + state.writeRegister(Registers.Index(raw: 7), HostCallResultCodeInner.OOG.rawValue) + case .panic: + state.writeRegister(Registers.Index(raw: 7), HostCallResultCodeInner.PANIC.rawValue) + case .halt: + state.writeRegister(Registers.Index(raw: 7), HostCallResultCodeInner.HALT.rawValue) + } + } +} + +/// Expunge an inner PVM +public class Expunge: HostCall { + public static var identifier: UInt8 { 24 } + + public var context: RefineContext.ContextType + + public init(context: inout RefineContext.ContextType) { + self.context = context + } + + public func _callImpl(config _: ProtocolConfigRef, state: VMState) async throws { + let reg7: UInt64 = state.readRegister(Registers.Index(raw: 7)) + + if context.pvms[reg7] == nil { + state.writeRegister(Registers.Index(raw: 7), HostCallResultCode.WHO.rawValue) + return + } + + state.writeRegister(Registers.Index(raw: 7), context.pvms[reg7]!.pc) + context.pvms[reg7] = nil + } +} diff --git a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/ResultConstants.swift b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/ResultConstants.swift index 5bfb1256..7372f85c 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/ResultConstants.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/ResultConstants.swift @@ -33,4 +33,6 @@ public enum HostCallResultCodeInner: UInt32 { case FAULT = 2 /// HOST = 3: The invocation completed with a host-call fault. case HOST = 3 + /// OOG = 4: The invocation completed by running out of gas. + case OOG = 4 } diff --git a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift index 8842a8e0..90039a4e 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/AccumulateContext.swift @@ -7,16 +7,17 @@ private let logger = Logger(label: "AccumulateContext") public class AccumulateContext: InvocationContext { public typealias ContextType = ( x: AccumlateResultContext, - y: AccumlateResultContext, // only set in checkpoint host-call - timeslot: TimeslotIndex + y: AccumlateResultContext // only set in checkpoint host-call ) - public var config: ProtocolConfigRef + public let config: ProtocolConfigRef public var context: ContextType + public let timeslot: TimeslotIndex - public init(context: inout ContextType, config: ProtocolConfigRef) { + public init(context: inout ContextType, config: ProtocolConfigRef, timeslot: TimeslotIndex) { self.config = config self.context = context + self.timeslot = timeslot } public func dispatch(index: UInt32, state: VMState) async -> ExecOutcome { @@ -57,9 +58,9 @@ public class AccumulateContext: InvocationContext { return await Quit(x: &context.x) .call(config: config, state: state) case Solicit.identifier: - return await Solicit(x: &context.x, timeslot: context.timeslot).call(config: config, state: state) + return await Solicit(x: &context.x, timeslot: timeslot).call(config: config, state: state) case Forget.identifier: - return await Forget(x: &context.x, timeslot: context.timeslot).call(config: config, state: state) + return await Forget(x: &context.x, timeslot: timeslot).call(config: config, state: state) default: state.consumeGas(Gas(10)) state.writeRegister(Registers.Index(raw: 0), HostCallResultCode.WHAT.rawValue) diff --git a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/OnTransferContext.swift b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/OnTransferContext.swift index ec8f2e83..99fd1689 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/OnTransferContext.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/OnTransferContext.swift @@ -10,7 +10,7 @@ public class OnTransferContext: InvocationContext { accounts: ServiceAccounts ) - public var config: ProtocolConfigRef + public let config: ProtocolConfigRef public var context: ContextType public init(context: inout ContextType, config: ProtocolConfigRef) { diff --git a/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/RefineContext.swift b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/RefineContext.swift new file mode 100644 index 00000000..da7ea091 --- /dev/null +++ b/Blockchain/Sources/Blockchain/VMInvocations/InvocationContexts/RefineContext.swift @@ -0,0 +1,82 @@ +import Foundation +import PolkaVM +import TracingUtils + +private let logger = Logger(label: "RefineContext") + +public struct InnerPvm { + public var code: Data + public var memory: Memory + public var pc: UInt32 +} + +public class RefineContext: InvocationContext { + public typealias ContextType = ( + pvms: [UInt64: InnerPvm], + exports: [Data] + ) + + public let config: ProtocolConfigRef + public var context: ContextType + public let importSegments: [Data] + public let exportSegmentOffset: UInt64 + public let service: ServiceIndex + public let serviceAccounts: ServiceAccounts + public let lookupAnchorTimeslot: TimeslotIndex + + public init( + config: ProtocolConfigRef, + context: ContextType, + importSegments: [Data], + exportSegmentOffset: UInt64, + service: ServiceIndex, + serviceAccounts: some ServiceAccounts, + lookupAnchorTimeslot: TimeslotIndex + ) { + self.config = config + self.context = context + self.importSegments = importSegments + self.exportSegmentOffset = exportSegmentOffset + self.service = service + self.serviceAccounts = serviceAccounts + self.lookupAnchorTimeslot = lookupAnchorTimeslot + } + + public func dispatch(index: UInt32, state: VMState) async -> ExecOutcome { + logger.debug("dispatching host-call: \(index)") + + if index == GasFn.identifier { + return await GasFn().call(config: config, state: state) + } else if index == HistoricalLookup.identifier { + return await HistoricalLookup( + context: context, + service: service, + serviceAccounts: serviceAccounts, + lookupAnchorTimeslot: lookupAnchorTimeslot + ) + .call(config: config, state: state) + } else if index == Import.identifier { + return await Import(context: context, importSegments: importSegments).call(config: config, state: state) + } else if index == Export.identifier { + return await Export(context: &context, exportSegmentOffset: exportSegmentOffset).call(config: config, state: state) + } else if index == Machine.identifier { + return await Machine(context: &context).call(config: config, state: state) + } else if index == Peek.identifier { + return await Peek(context: context).call(config: config, state: state) + } else if index == Zero.identifier { + return await Zero(context: &context).call(config: config, state: state) + } else if index == Poke.identifier { + return await Poke(context: &context).call(config: config, state: state) + } else if index == VoidFn.identifier { + return await VoidFn(context: &context).call(config: config, state: state) + } else if index == Invoke.identifier { + return await Invoke(context: &context).call(config: config, state: state) + } else if index == Expunge.identifier { + return await Expunge(context: &context).call(config: config, state: state) + } else { + state.consumeGas(Gas(10)) + 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 f129f0aa..caba9677 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/Invocations/AccumulateInvocation.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/AccumulateInvocation.swift @@ -31,11 +31,10 @@ extension AccumulateFunction { var contextContent = AccumulateContext.ContextType( x: resultCtx, - y: resultCtx, - timeslot: timeslot + y: resultCtx ) - let ctx = AccumulateContext(context: &contextContent, config: config) - let argument = try JamEncoder.encode(arguments) + let ctx = AccumulateContext(context: &contextContent, config: config, timeslot: timeslot) + let argument = try JamEncoder.encode(timeslot) + JamEncoder.encode(serviceIndex) + JamEncoder.encode(arguments) let (exitReason, gas, output) = await invokePVM( config: config, diff --git a/Blockchain/Sources/Blockchain/VMInvocations/Invocations/OnTransferInvocation.swift b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/OnTransferInvocation.swift index fedc57e4..64b5004c 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/Invocations/OnTransferInvocation.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/OnTransferInvocation.swift @@ -8,6 +8,7 @@ extension OnTransferFunction { config: ProtocolConfigRef, service: ServiceIndex, serviceAccounts: inout some ServiceAccounts, + timeslot: TimeslotIndex, transfers: [DeferredTransfers] ) async throws { guard var account = try await serviceAccounts.get(serviceAccount: service) else { @@ -23,7 +24,7 @@ extension OnTransferFunction { var contextContent = OnTransferContext.ContextType(service, serviceAccounts) let ctx = OnTransferContext(context: &contextContent, config: config) let gasLimitSum = transfers.reduce(Balance(0)) { $0 + $1.gasLimit } - let argument = try JamEncoder.encode(transfers) + let argument = try JamEncoder.encode(timeslot) + JamEncoder.encode(service) + JamEncoder.encode(transfers) _ = await invokePVM( config: config, diff --git a/Blockchain/Sources/Blockchain/VMInvocations/Invocations/RefineInvocation.swift b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/RefineInvocation.swift new file mode 100644 index 00000000..2906dd4c --- /dev/null +++ b/Blockchain/Sources/Blockchain/VMInvocations/Invocations/RefineInvocation.swift @@ -0,0 +1,93 @@ +import Codec +import Foundation +import PolkaVM +import Utils + +public protocol RefineInvocation { + func invoke( + config: ProtocolConfigRef, + serviceAccounts: some ServiceAccounts, + codeHash: Data, + gas: Gas, + service: ServiceIndex, + workPackageHash: Data32, + workPayload: Data, + refinementCtx: RefinementContext, + authorizerHash: Data32, + authorizationOutput: Data, + importSegments: [Data], // array of Data4104 + extrinsicDataBlobs: [Data], + exportSegmentOffset: UInt64 + ) async throws -> (result: Result, exports: [Data]) +} + +extension RefineInvocation { + func invoke( + config: ProtocolConfigRef, + serviceAccounts: some ServiceAccounts, + codeHash: Data32, + gas: Gas, + service: ServiceIndex, + workPackageHash: Data32, + workPayload: Data, // y + refinementCtx: RefinementContext, // c + authorizerHash: Data32, + authorizationOutput: Data, + importSegments: [Data], + extrinsicDataBlobs: [Data], + exportSegmentOffset: UInt64 + ) async throws -> (result: Result, exports: [Data]) { + let codeBlob = try await serviceAccounts.historicalLookup( + serviceAccount: service, + timeslot: refinementCtx.lookupAnchor.timeslot, + preimageHash: codeHash + ) + + guard let codeBlob, try await serviceAccounts.get(serviceAccount: service) != nil else { + return (.failure(.invalidCode), []) + } + + guard codeBlob.count <= config.value.maxServiceCodeSize else { + return (.failure(.codeTooLarge), []) + } + + let argumentData = try JamEncoder.encode(service) + + JamEncoder.encode(workPayload) + + JamEncoder.encode(workPackageHash) + + JamEncoder.encode(refinementCtx) + + JamEncoder.encode(authorizerHash) + + JamEncoder.encode(authorizationOutput) + + JamEncoder.encode(extrinsicDataBlobs) + let ctx = RefineContext( + config: config, + context: (pvms: [:], exports: []), + importSegments: importSegments, + exportSegmentOffset: exportSegmentOffset, + service: service, + serviceAccounts: serviceAccounts, + lookupAnchorTimeslot: refinementCtx.lookupAnchor.timeslot + ) + + let (exitReason, _, output) = await invokePVM( + config: config, + blob: codeBlob, + pc: 0, + gas: gas, + argumentData: argumentData, + ctx: ctx + ) + + switch exitReason { + case .outOfGas: + return (.failure(.outOfGas), []) + case .panic(.trap): + return (.failure(.panic), []) + default: + if let output { + return (.success(output), ctx.context.exports) + } else { + return (.failure(.panic), []) + } + } + } +} diff --git a/Node/Tests/NodeTests/chainfiles/devnet_allconfig_spec.json b/Node/Tests/NodeTests/chainfiles/devnet_allconfig_spec.json index 54546404..a44a2d30 100644 --- a/Node/Tests/NodeTests/chainfiles/devnet_allconfig_spec.json +++ b/Node/Tests/NodeTests/chainfiles/devnet_allconfig_spec.json @@ -14,8 +14,9 @@ "erasureCodedSegmentSize" : 6, "maxAuthorizationsPoolItems" : 8, "maxAuthorizationsQueueItems" : 10, + "segmentSize": 4104, "maxEncodedWorkPackageSize" : 12582912, - "maxEncodedWorkReportSize" : 98304, + "maxWorkReportOutputSize": 98304, "maxLookupAnchorAge" : 14400, "maxServiceCodeSize" : 4000000, "maxTicketsPerExtrinsic" : 4, diff --git a/PolkaVM/Sources/PolkaVM/Registers.swift b/PolkaVM/Sources/PolkaVM/Registers.swift index 79c70574..55032a57 100644 --- a/PolkaVM/Sources/PolkaVM/Registers.swift +++ b/PolkaVM/Sources/PolkaVM/Registers.swift @@ -128,3 +128,23 @@ public struct Registers: Equatable { } } } + +extension Registers: Codable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + var registers = [UInt64](repeating: 0, count: 13) + for i in 0 ..< 13 { + registers[i] = try container.decode(UInt64.self) + } + self.init(registers) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + for i in 0 ..< 13 { + try container.encode(self[Registers.Index(raw: UInt8(i))]) + } + } +} diff --git a/PolkaVM/Sources/PolkaVM/VMState.swift b/PolkaVM/Sources/PolkaVM/VMState.swift index 8e50df9a..5db7c318 100644 --- a/PolkaVM/Sources/PolkaVM/VMState.swift +++ b/PolkaVM/Sources/PolkaVM/VMState.swift @@ -40,6 +40,10 @@ public class VMState { Memory.Readonly(memory) } + public func getMemoryUnsafe() -> Memory { + memory + } + public func readMemory(address: some FixedWidthInteger) throws -> UInt8 { try memory.read(address: UInt32(truncatingIfNeeded: address)) }