diff --git a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift index 9cae7709..71751d82 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift @@ -1003,7 +1003,7 @@ public class Invoke: HostCall { 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) + let exitReason = await engine.execute(state: vm) try state.writeMemory(address: startAddr, values: JamEncoder.encode(vm.getGas(), vm.getRegisters())) context.pvms[pvmIndex]?.memory = vm.getMemoryUnsafe() diff --git a/PolkaVM/Sources/PolkaVM/Engine.swift b/PolkaVM/Sources/PolkaVM/Engine.swift index be53f1f7..d3d92889 100644 --- a/PolkaVM/Sources/PolkaVM/Engine.swift +++ b/PolkaVM/Sources/PolkaVM/Engine.swift @@ -13,13 +13,13 @@ public class Engine { self.invocationContext = invocationContext } - public func execute(program: ProgramCode, state: VMState) async -> ExitReason { + public func execute(state: VMState) async -> ExitReason { let context = ExecutionContext(state: state, config: config) while true { guard state.getGas() > GasInt(0) else { return .outOfGas } - if case let .exit(reason) = step(program: program, context: context) { + if case let .exit(reason) = step(program: state.program, context: context) { switch reason { case let .hostCall(callIndex): if case let .exit(hostExitReason) = await hostCall(state: state, callIndex: callIndex) { @@ -44,14 +44,14 @@ public class Engine { case let .pageFault(address): return .exit(.pageFault(address)) case let .hostCall(callIndexInner): - let pc = state.pc - let skip = state.program.skip(pc) - state.increasePC(skip + 1) return await hostCall(state: state, callIndex: callIndexInner) default: return .exit(reason) } case .continued: + let pc = state.pc + let skip = state.program.skip(pc) + state.increasePC(skip + 1) return .continued } } diff --git a/PolkaVM/Sources/PolkaVM/ExecOutcome.swift b/PolkaVM/Sources/PolkaVM/ExecOutcome.swift index 474990f0..2a70eecc 100644 --- a/PolkaVM/Sources/PolkaVM/ExecOutcome.swift +++ b/PolkaVM/Sources/PolkaVM/ExecOutcome.swift @@ -1,4 +1,4 @@ -public enum ExitReason { +public enum ExitReason: Equatable { public enum PanicReason { case trap case invalidInstructionIndex diff --git a/PolkaVM/Sources/PolkaVM/Memory.swift b/PolkaVM/Sources/PolkaVM/Memory.swift index ec06e116..5ff9e6c3 100644 --- a/PolkaVM/Sources/PolkaVM/Memory.swift +++ b/PolkaVM/Sources/PolkaVM/Memory.swift @@ -67,7 +67,7 @@ public protocol Memory { func read(address: UInt32) throws -> UInt8 func read(address: UInt32, length: Int) throws -> Data func write(address: UInt32, value: UInt8) throws - func write(address: UInt32, values: some Sequence) throws + func write(address: UInt32, values: Data) throws func zero(pageIndex: UInt32, pages: Int) throws func void(pageIndex: UInt32, pages: Int) throws @@ -186,13 +186,13 @@ public class MemoryChunk { guard startAddress <= address, address + UInt32(length) <= endAddress else { throw .exceedChunkBoundary(address) } - let startIndex = address - startAddress + let startIndex = Int(address - startAddress) + data.startIndex - if startIndex >= data.count { + if startIndex >= data.endIndex { return Data(repeating: 0, count: length) } else { - let validCount = min(length, data.count - Int(startIndex)) - let dataToRead = data.count > 0 ? data[startIndex ..< startIndex + UInt32(validCount)] : Data() + let validCount = min(length, data.endIndex - startIndex) + let dataToRead = data.count > 0 ? data[startIndex ..< startIndex + validCount] : Data() let zeroCount = max(0, length - validCount) let zeros = Data(repeating: 0, count: zeroCount) @@ -201,18 +201,17 @@ public class MemoryChunk { } } - public func write(address: UInt32, values: some Sequence) throws(MemoryError) { - let valuesData = Data(values) - guard startAddress <= address, address + UInt32(valuesData.count) <= endAddress else { + public func write(address: UInt32, values: Data) throws(MemoryError) { + guard startAddress <= address, address + UInt32(values.count) <= endAddress else { throw .exceedChunkBoundary(address) } - let startIndex = address - startAddress - let endIndex = startIndex + UInt32(valuesData.count) + let startIndex = Int(address - startAddress) + data.startIndex + let endIndex = startIndex + values.count - try zeroPad(until: startAddress + endIndex) + try zeroPad(until: startAddress + UInt32(endIndex)) - data[startIndex ..< endIndex] = valuesData + data.replaceSubrange(startIndex ..< endIndex, with: values) } public func incrementEnd(size increment: UInt32) throws(MemoryError) { @@ -335,11 +334,11 @@ public class StandardMemory: Memory { guard isWritable(address: address, length: 1) else { throw .notWritable(address) } - try getChunk(address: address).write(address: address, values: [value]) + try getChunk(address: address).write(address: address, values: Data([value])) } - public func write(address: UInt32, values: some Sequence) throws(MemoryError) { - guard isWritable(address: address, length: values.underestimatedCount) else { + public func write(address: UInt32, values: Data) throws(MemoryError) { + guard isWritable(address: address, length: values.count) else { throw .notWritable(address) } try getChunk(address: address).write(address: address, values: values) @@ -488,11 +487,11 @@ public class GeneralMemory: Memory { guard isWritable(address: address, length: 1) else { throw .notWritable(address) } - try getChunk(address: address).write(address: address, values: [value]) + try getChunk(address: address).write(address: address, values: Data([value])) } - public func write(address: UInt32, values: some Sequence) throws(MemoryError) { - guard isWritable(address: address, length: values.underestimatedCount) else { + public func write(address: UInt32, values: Data) throws(MemoryError) { + guard isWritable(address: address, length: values.count) else { throw .notWritable(address) } try getChunk(address: address).write(address: address, values: values) diff --git a/PolkaVM/Sources/PolkaVM/ProgramCode.swift b/PolkaVM/Sources/PolkaVM/ProgramCode.swift index e91c8d66..4069c029 100644 --- a/PolkaVM/Sources/PolkaVM/ProgramCode.swift +++ b/PolkaVM/Sources/PolkaVM/ProgramCode.swift @@ -146,7 +146,7 @@ public class ProgramCode { var value: UInt32 = 0 if (beginIndex + 4) < bitmask.endIndex { // if enough bytes - value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex, as: UInt32.self) } + value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex - bitmask.startIndex, as: UInt32.self) } } else { let byte1 = UInt32(bitmask[beginIndex]) let byte2 = UInt32(bitmask[safe: beginIndex + 1] ?? 0xFF) diff --git a/PolkaVM/Sources/PolkaVM/VMState.swift b/PolkaVM/Sources/PolkaVM/VMState.swift index 78d1de5a..e43e3d99 100644 --- a/PolkaVM/Sources/PolkaVM/VMState.swift +++ b/PolkaVM/Sources/PolkaVM/VMState.swift @@ -65,7 +65,7 @@ public class VMState { } public func writeMemory(address: some FixedWidthInteger, values: some Sequence) throws { - try memory.write(address: UInt32(truncatingIfNeeded: address), values: values) + try memory.write(address: UInt32(truncatingIfNeeded: address), values: Data(values)) } public func sbrk(_ increment: UInt32) throws -> UInt32 { diff --git a/PolkaVM/Sources/PolkaVM/invokePVM.swift b/PolkaVM/Sources/PolkaVM/invokePVM.swift index a57d8326..bfdbce73 100644 --- a/PolkaVM/Sources/PolkaVM/invokePVM.swift +++ b/PolkaVM/Sources/PolkaVM/invokePVM.swift @@ -11,12 +11,12 @@ public func invokePVM( pc: UInt32, gas: Gas, argumentData: Data?, - ctx: any InvocationContext + ctx: (any InvocationContext)? ) async -> (ExitReason, Gas, Data?) { do { let state = try VMState(standardProgramBlob: blob, pc: pc, gas: gas, argumentData: argumentData) let engine = Engine(config: config, invocationContext: ctx) - let exitReason = await engine.execute(program: state.program, state: state) + let exitReason = await engine.execute(state: state) switch exitReason { case .outOfGas: diff --git a/PolkaVM/Tests/PolkaVMTests/InvokePVMTest.swift b/PolkaVM/Tests/PolkaVMTests/InvokePVMTest.swift new file mode 100644 index 00000000..3cdd8bd6 --- /dev/null +++ b/PolkaVM/Tests/PolkaVMTests/InvokePVMTest.swift @@ -0,0 +1,139 @@ +import Foundation +import Testing +import Utils + +@testable import PolkaVM + +// standard programs +let empty = Data([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0]) +let fibonacci = Data([ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 61, 0, 0, 0, 0, 0, 51, 128, 119, 0, + 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200, + 152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 61, 7, 0, 0, 2, 0, + 51, 8, 4, 51, 7, 0, 0, 2, 0, 1, 50, 0, 73, 154, 148, 170, 130, 4, 3, +]) +let sumToN = Data([ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 38, 128, 119, 0, + 51, 8, 0, 100, 121, 40, 3, 0, 200, 137, 8, 149, 153, 255, 86, 9, 250, + 61, 8, 0, 0, 2, 0, 51, 8, 4, 51, 7, 0, 0, 2, 0, 1, 50, 0, 73, 77, 18, + 36, 24, +]) +let sumToNWithHostCall = Data([ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 40, 128, 119, 0, + 51, 8, 0, 100, 121, 40, 3, 0, 200, 137, 8, 149, 153, 255, 86, 9, 250, + 61, 8, 0, 0, 2, 0, 51, 8, 4, 51, 7, 0, 0, 2, 0, 10, 1, 1, 50, 0, 73, + 77, 18, 36, 104, +]) + +struct InvokePVMTests { + @Test func testEmptyProgram() async throws { + let config = DefaultPvmConfig() + let (exitReason, gas, output) = await invokePVM( + config: config, + blob: empty, + pc: 0, + gas: Gas(1_000_000), + argumentData: Data(), + ctx: nil + ) + #expect(exitReason == .panic(.trap)) + #expect(gas == Gas(0)) + #expect(output == nil) + } + + @Test(arguments: [ + (2, 2, 999_980), + (8, 34, 999_944), + (9, 55, 999_938), + ]) + func testFibonacci(testCase: (input: UInt8, output: UInt8, gas: UInt64)) async throws { + let config = DefaultPvmConfig() + let (exitReason, gas, output) = await invokePVM( + config: config, + blob: fibonacci, + pc: 0, + gas: Gas(1_000_000), + argumentData: Data([testCase.input]), + ctx: nil + ) + + let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0 + + switch exitReason { + case .halt: + #expect(value == testCase.output) + #expect(gas == Gas(testCase.gas)) + default: + Issue.record("Expected halt, got \(exitReason)") + } + } + + @Test(arguments: [ + (1, 1, 999_988), + (4, 10, 999_979), + (5, 15, 999_976), + ]) + func testSumToN(testCase: (input: UInt8, output: UInt8, gas: UInt64)) async throws { + let config = DefaultPvmConfig() + let (exitReason, gas, output) = await invokePVM( + config: config, + blob: sumToN, + pc: 0, + gas: Gas(1_000_000), + argumentData: Data([testCase.input]), + ctx: nil + ) + + let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0 + + switch exitReason { + case .halt: + #expect(value == testCase.output) + #expect(gas == Gas(testCase.gas)) + default: + Issue.record("Expected halt, got \(exitReason)") + } + } + + @Test func testInvocationContext() async throws { + let config = DefaultPvmConfig() + + struct TestInvocationContext: InvocationContext { + public typealias ContextType = Void + + public var context: ContextType = () + + public func dispatch(index _: UInt32, state: VMState) async -> ExecOutcome { + // perform output * 2 + do { + let (ouputAddr, len): (UInt32, UInt32) = state.readRegister(Registers.Index(raw: 7), Registers.Index(raw: 8)) + let output = try state.readMemory(address: ouputAddr, length: Int(len)) + let value = output.withUnsafeBytes { $0.load(as: UInt32.self) } + let newOutput = withUnsafeBytes(of: value << 1) { Data($0) } + try state.writeMemory(address: ouputAddr, values: newOutput) + return .continued + } catch { + return .exit(.panic(.trap)) + } + } + } + + let (exitReason, _, output) = await invokePVM( + config: config, + blob: sumToNWithHostCall, + pc: 0, + gas: Gas(1_000_000), + argumentData: Data([5]), + ctx: TestInvocationContext() + ) + + let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0 + + switch exitReason { + case .halt: + #expect(value == 30) + default: + Issue.record("Expected halt, got \(exitReason)") + } + } +} diff --git a/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift b/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift index 9f7e4469..9431dd51 100644 --- a/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift +++ b/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift @@ -80,7 +80,7 @@ enum MemoryTests { @Test func write() throws { let chunk = try MemoryChunk(startAddress: 0, endAddress: 10, data: Data()) - try chunk.write(address: 0, values: [1]) + try chunk.write(address: 0, values: Data([1])) #expect(chunk.data == Data([1])) try chunk.write(address: 1, values: Data([2])) #expect(chunk.data == Data([1, 2])) @@ -199,7 +199,7 @@ enum MemoryTests { #expect(memory.isWritable(address: stackStart, length: Int(stackEnd - stackStart)) == true) try memory.write(address: stackStart, value: 1) #expect(try memory.read(address: stackStart, length: 2) == Data([1, 0])) - try memory.write(address: stackEnd - 2, values: [1, 2]) + try memory.write(address: stackEnd - 2, values: Data([1, 2])) #expect(try memory.read(address: stackEnd - 4, length: 4) == Data([0, 0, 1, 2])) // argument @@ -260,9 +260,9 @@ enum MemoryTests { } @Test func write() throws { - try memory.write(address: 2, values: [9, 8]) + try memory.write(address: 2, values: Data([9, 8])) #expect(try memory.read(address: 0, length: 4) == Data([1, 2, 9, 8])) - #expect(throws: MemoryError.notWritable(4096)) { try memory.write(address: 4096, values: [0]) } + #expect(throws: MemoryError.notWritable(4096)) { try memory.write(address: 4096, values: Data([0])) } } @Test func sbrk() throws { @@ -271,7 +271,7 @@ enum MemoryTests { #expect(memory.isWritable(address: oldEnd, length: 512) == true) #expect(memory.isWritable(address: 0, length: Int(oldEnd)) == true) - try memory.write(address: oldEnd, values: [1, 2, 3]) + try memory.write(address: oldEnd, values: Data([1, 2, 3])) #expect(try memory.read(address: oldEnd - 1, length: 5) == Data([7, 1, 2, 3, 0])) } diff --git a/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift b/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift index f949eba6..ec21a9ec 100644 --- a/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift +++ b/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift @@ -47,8 +47,6 @@ struct ProgramTests { _ = try ProgramCode(data) } - // TODO: add more Program parsing tests - @Test(arguments: [ (Data(), 0, 0), (Data([0]), 0, 7), @@ -66,4 +64,22 @@ struct ProgramTests { func skip(testCase: (Data, UInt32, UInt32)) { #expect(ProgramCode.skip(start: testCase.1, bitmask: testCase.0) == testCase.2) } + + @Test(arguments: [ + // inst_branch_eq_imm_nok + Data([0, 0, 16, 51, 7, 210, 4, 81, 39, 211, 4, 6, 0, 51, 7, 239, 190, 173, 222, 17, 6]), + // inst_branch_greater_unsigned_imm_ok + Data([0, 0, 14, 51, 7, 246, 86, 23, 10, 5, 0, 51, 7, 239, 190, 173, 222, 137, 1]), + // fibonacci general program (from pvm debuger example) + Data([ + 0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, + 200, 152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0, + ]) + ]) + func parseProgramCode(testCase: Data) throws { + let program = try ProgramCode(testCase) + #expect(program.jumpTableEntrySize == 0) + #expect(program.jumpTable == Data()) + #expect(program.code == testCase[3 ..< testCase[2] + 3]) + } }