diff --git a/Blockchain/Package.swift b/Blockchain/Package.swift index 02c0abda..8263e990 100644 --- a/Blockchain/Package.swift +++ b/Blockchain/Package.swift @@ -19,6 +19,7 @@ let package = Package( .package(path: "../Codec"), .package(path: "../Utils"), .package(path: "../TracingUtils"), + .package(path: "../PolkaVM"), .package(url: "https://github.com/apple/swift-testing.git", branch: "0.10.0"), ], targets: [ @@ -29,6 +30,7 @@ let package = Package( dependencies: [ "Codec", "Utils", + "PolkaVM", "TracingUtils", ] ), diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift index dc249393..9fe079f8 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig+Preset.swift @@ -33,7 +33,7 @@ extension Ref where T == ProtocolConfig { maxEncodedWorkReportSize: 96 * 1 << 10, erasureCodedSegmentSize: 6, ticketSubmissionEndSlot: 500, - pvmDynamicAddressAlignmentFactor: 4, + pvmDynamicAddressAlignmentFactor: 2, pvmProgramInitInputDataSize: 1 << 24, pvmProgramInitPageSize: 1 << 14, pvmProgramInitSegmentSize: 1 << 16 @@ -70,7 +70,7 @@ extension Ref where T == ProtocolConfig { maxEncodedWorkReportSize: 96 * 1 << 10, erasureCodedSegmentSize: 6, ticketSubmissionEndSlot: 500, - pvmDynamicAddressAlignmentFactor: 4, + pvmDynamicAddressAlignmentFactor: 2, pvmProgramInitInputDataSize: 1 << 24, pvmProgramInitPageSize: 1 << 14, pvmProgramInitSegmentSize: 1 << 16 diff --git a/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift b/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift index c0ba1607..5f074c3b 100644 --- a/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift +++ b/Blockchain/Sources/Blockchain/Config/ProtocolConfig.swift @@ -1,3 +1,4 @@ +import PolkaVM import Utils // constants defined in the graypaper @@ -94,7 +95,7 @@ public struct ProtocolConfig: Sendable { // Y = 500: The number of slots into an epoch at which ticket-submission ends. public var ticketSubmissionEndSlot: Int - // ZA = 4: The pvm dynamic address alignment factor. + // ZA = 2: The pvm dynamic address alignment factor. public var pvmDynamicAddressAlignmentFactor: Int // ZI = 2^24: The standard pvm program initialization input data size. @@ -181,6 +182,8 @@ public struct ProtocolConfig: Sendable { public typealias ProtocolConfigRef = Ref +extension ProtocolConfig: PvmConfig {} + extension ProtocolConfig { public enum AuditTranchePeriod: ReadInt { public typealias TConfig = ProtocolConfigRef diff --git a/Blockchain/Sources/Blockchain/Runtime.swift b/Blockchain/Sources/Blockchain/Runtime.swift index 175080c9..c068f11e 100644 --- a/Blockchain/Sources/Blockchain/Runtime.swift +++ b/Blockchain/Sources/Blockchain/Runtime.swift @@ -73,20 +73,9 @@ public final class Runtime { var newState = prevState.value do { - newState.recentHistory = try updateRecentHistory(block: block, state: prevState) + try updateRecentHistory(block: block, state: &newState) - let safroleResult = try newState.updateSafrole( - config: config, slot: block.header.timeslot, entropy: newState.entropyPool.t0, extrinsics: block.extrinsic.tickets - ) - newState.mergeWith(postState: safroleResult.state) - - guard safroleResult.epochMark == block.header.epoch else { - throw Error.invalidHeaderEpochMarker - } - - guard safroleResult.ticketsMark == block.header.winningTickets else { - throw Error.invalidHeaderWinningTickets - } + try updateSafrole(block: block, state: &newState) newState.coreAuthorizationPool = try updateAuthorizationPool( block: block, state: prevState @@ -106,30 +95,33 @@ public final class Runtime { return StateRef(newState) } - public func updateRecentHistory(block: BlockRef, state: StateRef) throws -> RecentHistory { - var history = state.value.recentHistory - if history.items.count >= 0 { // if this is not block #0 - // write the state root of last block - history.items[history.items.endIndex - 1].stateRoot = state.stateRoot - } - - let workReportHashes = block.extrinsic.reports.guarantees.map(\.workReport.packageSpecification.workPackageHash) + public func updateSafrole(block: BlockRef, state newState: inout State) throws { + let safroleResult = try newState.updateSafrole( + config: config, + slot: block.header.timeslot, + entropy: newState.entropyPool.t0, + offenders: newState.judgements.punishSet, + extrinsics: block.extrinsic.tickets + ) + newState.mergeWith(postState: safroleResult.state) - let accumulationResult = Data32() // TODO: calculate accumulation result + guard safroleResult.epochMark == block.header.epoch else { + throw Error.invalidHeaderEpochMarker + } - var mmr = history.items.last?.mmr ?? .init([]) - mmr.append(accumulationResult, hasher: Keccak.self) + guard safroleResult.ticketsMark == block.header.winningTickets else { + throw Error.invalidHeaderWinningTickets + } + } - let newItem = try RecentHistory.HistoryItem( + public func updateRecentHistory(block: BlockRef, state newState: inout State) throws { + let workReportHashes = block.extrinsic.reports.guarantees.map(\.workReport.packageSpecification.workPackageHash) + try newState.recentHistory.update( headerHash: block.header.parentHash, - mmr: mmr, - stateRoot: Data32(), // empty and will be updated upon next block + parentStateRoot: block.header.priorStateRoot, + accumulateRoot: Data32(), // TODO: calculate accumulation result workReportHashes: ConfigLimitedSizeArray(config: config, array: workReportHashes) ) - - history.items.safeAppend(newItem) - - return history } // TODO: add tests diff --git a/Blockchain/Sources/Blockchain/Safrole.swift b/Blockchain/Sources/Blockchain/Safrole.swift index 33867eca..6257e314 100644 --- a/Blockchain/Sources/Blockchain/Safrole.swift +++ b/Blockchain/Sources/Blockchain/Safrole.swift @@ -151,7 +151,13 @@ public protocol Safrole { > { get } var ticketsVerifier: BandersnatchRingVRFRoot { get } - func updateSafrole(config: ProtocolConfigRef, slot: TimeslotIndex, entropy: Data32, extrinsics: ExtrinsicTickets) throws(SafroleError) + func updateSafrole( + config: ProtocolConfigRef, + slot: TimeslotIndex, + entropy: Data32, + offenders: Set, + extrinsics: ExtrinsicTickets + ) throws(SafroleError) -> ( state: SafrolePostState, epochMark: EpochMarker?, @@ -207,9 +213,30 @@ func pickFallbackValidators( return indices.map { validators[$0].bandersnatch } } +func withoutOffenders( + offenders: Set, + validators: ConfigFixedSizeArray +) -> ConfigFixedSizeArray { + var validators = validators + + for i in validators.indices { + let validator = validators[i] + if offenders.contains(validator.ed25519) { + validators[i] = ValidatorKey() // replace to empty key + } + } + + return validators +} + extension Safrole { - public func updateSafrole(config: ProtocolConfigRef, slot: TimeslotIndex, entropy: Data32, - extrinsics: ExtrinsicTickets) throws(SafroleError) + public func updateSafrole( + config: ProtocolConfigRef, + slot: TimeslotIndex, + entropy: Data32, + offenders: Set, + extrinsics: ExtrinsicTickets + ) throws(SafroleError) -> ( state: SafrolePostState, epochMark: EpochMarker?, @@ -250,7 +277,7 @@ extension Safrole { let (newNextValidators, newCurrentValidators, newPreviousValidators, newTicketsVerifier) = isEpochChange ? ( - validatorQueue, // TODO: Φ filter out the one in the punishment set + withoutOffenders(offenders: offenders, validators: validatorQueue), nextValidators, currentValidators, newVerifier.ringRoot diff --git a/Blockchain/Sources/Blockchain/Types/RecentHistory.swift b/Blockchain/Sources/Blockchain/Types/RecentHistory.swift index fc76ac0f..c8c12936 100644 --- a/Blockchain/Sources/Blockchain/Types/RecentHistory.swift +++ b/Blockchain/Sources/Blockchain/Types/RecentHistory.swift @@ -37,3 +37,29 @@ extension RecentHistory: Dummy { RecentHistory(items: try! ConfigLimitedSizeArray(config: config)) } } + +extension RecentHistory { + public mutating func update( + headerHash: Data32, + parentStateRoot: Data32, + accumulateRoot: Data32, + workReportHashes: ConfigLimitedSizeArray + ) { + if items.count > 0 { // if this is not block #0 + // write the state root of last block + items[items.endIndex - 1].stateRoot = parentStateRoot + } + + var mmr = items.last?.mmr ?? .init([]) + mmr.append(accumulateRoot, hasher: Keccak.self) + + let newItem = RecentHistory.HistoryItem( + headerHash: headerHash, + mmr: mmr, + stateRoot: Data32(), // empty and will be updated upon next block + workReportHashes: workReportHashes + ) + + items.safeAppend(newItem) + } +} diff --git a/Blockchain/Sources/Blockchain/Types/ValidatorKey.swift b/Blockchain/Sources/Blockchain/Types/ValidatorKey.swift index d0ad46cf..9c9dde0a 100644 --- a/Blockchain/Sources/Blockchain/Types/ValidatorKey.swift +++ b/Blockchain/Sources/Blockchain/Types/ValidatorKey.swift @@ -17,16 +17,18 @@ public struct ValidatorKey: Sendable, Equatable, Codable { self.bls = bls self.metadata = metadata } + + public init() { + bandersnatch = BandersnatchPublicKey() + ed25519 = Ed25519PublicKey() + bls = BLSKey() + metadata = Data128() + } } extension ValidatorKey: Dummy { public typealias Config = ProtocolConfigRef public static func dummy(config _: Config) -> ValidatorKey { - ValidatorKey( - bandersnatch: BandersnatchPublicKey(), - ed25519: Ed25519PublicKey(), - bls: BLSKey(), - metadata: Data128() - ) + ValidatorKey() } } diff --git a/Codec/Sources/Codec/JamDecoder.swift b/Codec/Sources/Codec/JamDecoder.swift index b6d9252d..0afd76bd 100644 --- a/Codec/Sources/Codec/JamDecoder.swift +++ b/Codec/Sources/Codec/JamDecoder.swift @@ -324,8 +324,11 @@ private struct JamUnkeyedDecodingContainer: UnkeyedDecodingContainer { let decoder: DecodeContext mutating func decodeNil() throws -> Bool { + guard let byte = decoder.data.next() else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "Unexpected end of data") + } currentIndex += 1 - return decoder.data.count > 0 + return byte == 0 } mutating func decode(_: Bool.Type) throws -> Bool { @@ -439,7 +442,7 @@ private struct JamSingleValueDecodingContainer: SingleValueDecodingContainer { let decoder: DecodeContext func decodeNil() -> Bool { - decoder.data.count > 0 + decoder.data.next() == 0 } func decode(_: Bool.Type) throws -> Bool { diff --git a/JAMTests/Sources/JAMTests/TestLoader.swift b/JAMTests/Sources/JAMTests/TestLoader.swift index 2c6b961b..9d5619b9 100644 --- a/JAMTests/Sources/JAMTests/TestLoader.swift +++ b/JAMTests/Sources/JAMTests/TestLoader.swift @@ -3,12 +3,20 @@ import Foundation // somehow without this the GH Actions CI fails extension Foundation.Bundle: @unchecked @retroactive Sendable {} +struct Testcase: CustomStringConvertible { + var description: String + var data: Data +} + enum TestLoader { - static func getTestFiles(path: String, extension ext: String) throws -> [(path: String, description: String)] { + static func getTestcases(path: String, extension ext: String) throws -> [Testcase] { let prefix = Bundle.module.resourcePath! + "/jamtestvectors/\(path)" let files = try FileManager.default.contentsOfDirectory(atPath: prefix) var filtered = files.filter { $0.hasSuffix(".\(ext)") } filtered.sort() - return filtered.map { (path: prefix + "/" + $0, description: $0) } + return try filtered.map { + let data = try Data(contentsOf: URL(fileURLWithPath: prefix + "/" + $0)) + return Testcase(description: $0, data: data) + } } } diff --git a/JAMTests/Tests/JAMTests/PVMTests.swift b/JAMTests/Tests/JAMTests/PVMTests.swift index 9ba9d051..0bbfa69e 100644 --- a/JAMTests/Tests/JAMTests/PVMTests.swift +++ b/JAMTests/Tests/JAMTests/PVMTests.swift @@ -62,17 +62,14 @@ struct PolkaVMTestcase: Codable, CustomStringConvertible { } struct PVMTests { - static func loadTests() throws -> [PolkaVMTestcase] { - let tests = try TestLoader.getTestFiles(path: "pvm/programs", extension: "json") - return try tests.map { - let data = try Data(contentsOf: URL(fileURLWithPath: $0.path)) - let decoder = JSONDecoder() - return try decoder.decode(PolkaVMTestcase.self, from: data) - } + static func loadTests() throws -> [Testcase] { + try TestLoader.getTestcases(path: "pvm/programs", extension: "json") } @Test(arguments: try loadTests()) - func testPVM(testCase: PolkaVMTestcase) throws { + func testPVM(testCase: Testcase) throws { + let decoder = JSONDecoder() + let testCase = try decoder.decode(PolkaVMTestcase.self, from: testCase.data) let program = try ProgramCode(Data(testCase.program)) let memory = Memory( pageMap: testCase.initialPageMap.map { (address: $0.address, length: $0.length, writable: $0.isWritable) }, diff --git a/JAMTests/Tests/JAMTests/RecentHistoryTests.swift b/JAMTests/Tests/JAMTests/RecentHistoryTests.swift new file mode 100644 index 00000000..22487ac1 --- /dev/null +++ b/JAMTests/Tests/JAMTests/RecentHistoryTests.swift @@ -0,0 +1,42 @@ +import Blockchain +import Codec +import Foundation +import Testing +import Utils + +@testable import JAMTests + +struct RecentHistoryInput: Codable { + var headerHash: Data32 + var parentStateRoot: Data32 + var accumulateRoot: Data32 + var workPackages: [Data32] +} + +struct RecentHisoryTestcase: Codable { + var input: RecentHistoryInput + var preState: RecentHistory + var postState: RecentHistory +} + +struct RecentHistoryTests { + static func loadTests() throws -> [Testcase] { + try TestLoader.getTestcases(path: "history/data", extension: "scale") + } + + @Test(arguments: try loadTests()) + func recentHistory(_ testcase: Testcase) throws { + let config = ProtocolConfigRef.mainnet + let testcase = try JamDecoder.decode(RecentHisoryTestcase.self, from: testcase.data, withConfig: config) + + var state = testcase.preState + try state.update( + headerHash: testcase.input.headerHash, + parentStateRoot: testcase.input.parentStateRoot, + accumulateRoot: testcase.input.accumulateRoot, + workReportHashes: ConfigLimitedSizeArray(config: config, array: testcase.input.workPackages) + ) + + #expect(state == testcase.postState) + } +} diff --git a/JAMTests/Tests/JAMTests/SafroleTests.swift b/JAMTests/Tests/JAMTests/SafroleTests.swift index 77188945..24d131a2 100644 --- a/JAMTests/Tests/JAMTests/SafroleTests.swift +++ b/JAMTests/Tests/JAMTests/SafroleTests.swift @@ -9,6 +9,7 @@ import Utils struct SafroleInput: Codable { var slot: UInt32 var entropy: Data32 + var offenders: [Ed25519PublicKey] var extrinsics: ExtrinsicTickets } @@ -93,11 +94,6 @@ struct SafroleTestcase: Codable { var postState: SafroleState } -struct Testcase: CustomStringConvertible { - var description: String - var data: Data -} - enum SafroleTestVariants: String, CaseIterable { case tiny case full @@ -121,13 +117,9 @@ enum SafroleTestVariants: String, CaseIterable { } } -final class SafroleTests { +struct SafroleTests { static func loadTests(variant: SafroleTestVariants) throws -> [Testcase] { - let tests = try TestLoader.getTestFiles(path: "safrole/\(variant)", extension: "scale") - return try tests.map { path, description in - let data = try Data(contentsOf: URL(fileURLWithPath: path)) - return Testcase(description: description, data: data) - } + try TestLoader.getTestcases(path: "safrole/\(variant)", extension: "scale") } func safroleTests(_ input: Testcase, variant: SafroleTestVariants) throws { @@ -139,6 +131,7 @@ final class SafroleTests { config: config, slot: testcase.input.slot, entropy: testcase.input.entropy, + offenders: Set(testcase.input.offenders), extrinsics: testcase.input.extrinsics ) } diff --git a/JAMTests/Tests/JAMTests/TrieTests.swift b/JAMTests/Tests/JAMTests/TrieTests.swift index c0097526..b41ee45b 100644 --- a/JAMTests/Tests/JAMTests/TrieTests.swift +++ b/JAMTests/Tests/JAMTests/TrieTests.swift @@ -12,17 +12,14 @@ struct TrieElement: Codable { typealias TrieTestCase = [TrieElement] struct TrieTests { - static func loadTests() throws -> [TrieTestCase] { - let tests = try TestLoader.getTestFiles(path: "trie", extension: "json") - return try tests.map { - let data = try Data(contentsOf: URL(fileURLWithPath: $0.path)) - let decoder = JSONDecoder() - return try decoder.decode(TrieTestCase.self, from: data) - } + static func loadTests() throws -> [Testcase] { + try TestLoader.getTestcases(path: "trie", extension: "json") } @Test(arguments: try loadTests()) - func trieTests(_ testcase: TrieTestCase) throws { + func trieTests(_ testcase: Testcase) throws { + let decoder = JSONDecoder() + let testcase = try decoder.decode(TrieTestCase.self, from: testcase.data) for element in testcase { let kv = element.input.reduce(into: [Data32: Data]()) { result, entry in let keyData = Data(fromHexString: entry.key) diff --git a/JAMTests/jamtestvectors b/JAMTests/jamtestvectors index 2a633e73..2da1072f 160000 --- a/JAMTests/jamtestvectors +++ b/JAMTests/jamtestvectors @@ -1 +1 @@ -Subproject commit 2a633e7398a29f2ccd167653591343dc6fc2ad90 +Subproject commit 2da1072f733c2f8aec2d6a354d565410ad8dd059 diff --git a/PolkaVM/Sources/PolkaVM/Config.swift b/PolkaVM/Sources/PolkaVM/Config.swift new file mode 100644 index 00000000..8828483d --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Config.swift @@ -0,0 +1,13 @@ +public protocol PvmConfig { + // ZA = 2: The pvm dynamic address alignment factor. + var pvmDynamicAddressAlignmentFactor: Int { get } + + // ZI = 2^24: The standard pvm program initialization input data size. + var pvmProgramInitInputDataSize: Int { get } + + // ZP = 2^14: The standard pvm program initialization page size. + var pvmProgramInitPageSize: Int { get } + + // ZQ = 2^16: The standard pvm program initialization segment size. + var pvmProgramInitSegmentSize: Int { get } +} diff --git a/PolkaVM/Sources/PolkaVM/Engine.swift b/PolkaVM/Sources/PolkaVM/Engine.swift index 5a247a0a..f9138f02 100644 --- a/PolkaVM/Sources/PolkaVM/Engine.swift +++ b/PolkaVM/Sources/PolkaVM/Engine.swift @@ -34,12 +34,6 @@ public class Engine { let res = inst.execute(state: state, skip: skip) - if state.pc == Constants.exitAddress { - // TODO: GP only defined this for `djump` but not `branch` - // so need to confirm this is correct - return .exit(.halt) - } - return res } } diff --git a/PolkaVM/Sources/PolkaVM/InstructionTable.swift b/PolkaVM/Sources/PolkaVM/InstructionTable.swift index 31673d04..7e5c830c 100644 --- a/PolkaVM/Sources/PolkaVM/InstructionTable.swift +++ b/PolkaVM/Sources/PolkaVM/InstructionTable.swift @@ -34,6 +34,63 @@ public class InstructionTable { Instructions.BranchLeSImm.self, Instructions.BranchGeSImm.self, Instructions.BranchGtSImm.self, + Instructions.MoveReg.self, + Instructions.Sbrk.self, + Instructions.StoreIndU8.self, + Instructions.StoreIndU16.self, + Instructions.StoreIndU32.self, + Instructions.LoadIndU8.self, + Instructions.LoadIndI8.self, + Instructions.LoadIndU16.self, + Instructions.LoadIndI16.self, + Instructions.LoadIndU32.self, + Instructions.AddImm.self, + Instructions.AndImm.self, + Instructions.XorImm.self, + Instructions.OrImm.self, + Instructions.MulImm.self, + Instructions.MulUpperSSImm.self, + Instructions.MulUpperUUImm.self, + Instructions.SetLtUImm.self, + Instructions.SetLtSImm.self, + Instructions.ShloLImm.self, + Instructions.ShloRImm.self, + Instructions.SharRImm.self, + Instructions.NegAddImm.self, + Instructions.SetGtUImm.self, + Instructions.SetGtSImm.self, + Instructions.ShloLImmAlt.self, + Instructions.ShloRImmAlt.self, + Instructions.SharRImmAlt.self, + Instructions.CmovIzImm.self, + Instructions.CmovNzImm.self, + Instructions.BranchEq.self, + Instructions.BranchNe.self, + Instructions.BranchLtU.self, + Instructions.BranchLtS.self, + Instructions.BranchGeU.self, + Instructions.BranchGeS.self, + Instructions.LoadImmJumpInd.self, + Instructions.Add.self, + Instructions.Sub.self, + Instructions.And.self, + Instructions.Xor.self, + Instructions.Or.self, + Instructions.Mul.self, + Instructions.MulUpperSS.self, + Instructions.MulUpperUU.self, + Instructions.MulUpperSU.self, + Instructions.DivU.self, + Instructions.DivS.self, + Instructions.RemU.self, + Instructions.RemS.self, + Instructions.SetLtU.self, + Instructions.SetLtS.self, + Instructions.ShloL.self, + Instructions.ShloR.self, + Instructions.SharR.self, + Instructions.CmovIz.self, + Instructions.CmovNz.self, ] var table: [Instruction.Type?] = Array(repeating: nil, count: 256) for i in 0 ..< insts.count { diff --git a/PolkaVM/Sources/PolkaVM/Instructions.swift b/PolkaVM/Sources/PolkaVM/Instructions.swift index 4d57f417..9fe0ebf2 100644 --- a/PolkaVM/Sources/PolkaVM/Instructions.swift +++ b/PolkaVM/Sources/PolkaVM/Instructions.swift @@ -7,14 +7,13 @@ public let BASIC_BLOCK_INSTRUCTIONS: Set = [ Instructions.Jump.opcode, Instructions.JumpInd.opcode, Instructions.LoadImmJump.opcode, - // TODO: uncomment after add more - // Instructions.LoadImmJumpInd.opcode, - // Instructions.BranchEq.opcode, - // Instructions.BranchNe.opcode, - // Instructions.BranchGeU.opcode, - // Instructions.BranchGeS.opcode, - // Instructions.BranchLtU.opcode, - // Instructions.BranchLtS.opcode, + Instructions.LoadImmJumpInd.opcode, + Instructions.BranchEq.opcode, + Instructions.BranchNe.opcode, + Instructions.BranchGeU.opcode, + Instructions.BranchGeS.opcode, + Instructions.BranchLtU.opcode, + Instructions.BranchLtS.opcode, Instructions.BranchEqImm.opcode, Instructions.BranchNeImm.opcode, Instructions.BranchLtUImm.opcode, @@ -28,6 +27,11 @@ public let BASIC_BLOCK_INSTRUCTIONS: Set = [ ] public enum Instructions { + public enum Constants { + public static let djumpHaltAddress: UInt32 = 0xFFFF_0000 + public static let djumpAddressAlignmentFactor: Int = 2 + } + static func decodeImmediate(_ data: Data) -> UInt32 { let len = min(data.count, 4) if len == 0 { @@ -44,10 +48,10 @@ public enum Instructions { return UInt32(bitPattern: Int32(bitPattern: value << shift) >> shift) } - static func decodeImmediate2(_ data: Data, divideBy: UInt8 = 1) throws -> (UInt32, UInt32) { + static func decodeImmediate2(_ data: Data, divideBy: UInt8 = 1, minus: Int = 1) throws -> (UInt32, UInt32) { let lX1 = try Int((data.at(relative: 0) / divideBy) & 0b111) let lX = min(4, lX1) - let lY = min(4, max(0, data.count - Int(lX) - 1)) + let lY = min(4, max(0, data.count - Int(lX) - minus)) let vX = try decodeImmediate(data.at(relative: 1 ..< 1 + lX)) let vY = try decodeImmediate(data.at(relative: (1 + lX) ..< (1 + lX + lY))) @@ -58,6 +62,35 @@ public enum Instructions { state.program.basicBlockIndices.contains(state.pc &+ offset) } + static func isDjumpValid(state: VMState, target a: UInt32, targetAligned: UInt32) -> Bool { + let za = Constants.djumpAddressAlignmentFactor + return !(a == 0 || + a > state.program.jumpTable.count * za || + Int(a) % za != 0 || + state.program.basicBlockIndices.contains(targetAligned)) + } + + static func djump(state: VMState, target: UInt32) -> ExecOutcome { + if target == Constants.djumpHaltAddress { + return .exit(.halt) + } + + let entrySize = Int(state.program.jumpTableEntrySize) + let start = ((Int(target) / Constants.djumpAddressAlignmentFactor) - 1) * entrySize + let end = start + entrySize + var targetAlignedData = state.program.jumpTable[relative: start ..< end] + guard let targetAligned = targetAlignedData.decode() else { + fatalError("unreachable: jump table entry should be valid") + } + + guard isDjumpValid(state: state, target: target, targetAligned: UInt32(truncatingIfNeeded: targetAligned)) else { + return .exit(.panic(.invalidDynamicJump)) + } + + state.updatePC(UInt32(targetAligned)) + return .continued + } + // MARK: Instructions without Arguments (5.1) public struct Trap: Instruction { @@ -195,8 +228,7 @@ public enum Instructions { public func updatePC(state: VMState, skip _: UInt32) -> ExecOutcome { let regVal = state.readRegister(register) - state.updatePC(regVal &+ offset) // wrapped add - return .continued + return Instructions.djump(state: state, target: regVal &+ offset) } } @@ -965,8 +997,8 @@ public enum Instructions { public func _executeImpl(state: VMState) -> ExecOutcome { let regVal = state.readRegister(rb) - let shift = value & 0x1F - state.writeRegister(ra, regVal << shift) + let shift = value & 0x20 + state.writeRegister(ra, UInt32(truncatingIfNeeded: regVal << shift)) return .continued } } @@ -986,7 +1018,7 @@ public enum Instructions { public func _executeImpl(state: VMState) -> ExecOutcome { let regVal = state.readRegister(rb) - let shift = value & 0x1F + let shift = value & 0x20 state.writeRegister(ra, regVal >> shift) return .continued } @@ -1007,7 +1039,7 @@ public enum Instructions { public func _executeImpl(state: VMState) -> ExecOutcome { let regVal = state.readRegister(rb) - let shift = value & 0x1F + let shift = value & 0x20 state.writeRegister(ra, UInt32(bitPattern: Int32(bitPattern: regVal) >> shift)) return .continued } @@ -1088,8 +1120,8 @@ public enum Instructions { public func _executeImpl(state: VMState) -> ExecOutcome { let regVal = state.readRegister(rb) - let shift = regVal & 0x1F - state.writeRegister(ra, value << shift) + let shift = regVal & 0x20 + state.writeRegister(ra, UInt32(truncatingIfNeeded: value << shift)) return .continued } } @@ -1109,7 +1141,7 @@ public enum Instructions { public func _executeImpl(state: VMState) -> ExecOutcome { let regVal = state.readRegister(rb) - let shift = regVal & 0x1F + let shift = regVal & 0x20 state.writeRegister(ra, value >> shift) return .continued } @@ -1130,13 +1162,13 @@ public enum Instructions { public func _executeImpl(state: VMState) -> ExecOutcome { let regVal = state.readRegister(rb) - let shift = regVal & 0x1F + let shift = regVal & 0x20 state.writeRegister(ra, UInt32(bitPattern: Int32(bitPattern: value) >> shift)) return .continued } } - public struct CmovIZImm: Instruction { + public struct CmovIzImm: Instruction { public static var opcode: UInt8 { 81 } public let ra: Registers.Index @@ -1156,7 +1188,7 @@ public enum Instructions { } } - public struct CmovNIImm: Instruction { + public struct CmovNzImm: Instruction { public static var opcode: UInt8 { 82 } public let ra: Registers.Index @@ -1175,6 +1207,544 @@ public enum Instructions { return .continued } } + + // MARK: Instructions with Arguments of Two Registers & One Offset (5.10) + + public struct BranchEq: BranchInstructionBase2 { + public static var opcode: UInt8 { 24 } + typealias Compare = CompareEq + + var r1: Registers.Index + var r2: Registers.Index + var offset: UInt32 + public init(data: Data) throws { (r1, r2, offset) = try Self.parse(data: data) } + } + + public struct BranchNe: BranchInstructionBase2 { + public static var opcode: UInt8 { 30 } + typealias Compare = CompareNe + + var r1: Registers.Index + var r2: Registers.Index + var offset: UInt32 + public init(data: Data) throws { (r1, r2, offset) = try Self.parse(data: data) } + } + + public struct BranchLtU: BranchInstructionBase2 { + public static var opcode: UInt8 { 47 } + typealias Compare = CompareLt + + var r1: Registers.Index + var r2: Registers.Index + var offset: UInt32 + public init(data: Data) throws { (r1, r2, offset) = try Self.parse(data: data) } + } + + public struct BranchLtS: BranchInstructionBase2 { + public static var opcode: UInt8 { 48 } + typealias Compare = CompareLt + + var r1: Registers.Index + var r2: Registers.Index + var offset: UInt32 + public init(data: Data) throws { (r1, r2, offset) = try Self.parse(data: data) } + } + + public struct BranchGeU: BranchInstructionBase2 { + public static var opcode: UInt8 { 41 } + typealias Compare = CompareGe + + var r1: Registers.Index + var r2: Registers.Index + var offset: UInt32 + public init(data: Data) throws { (r1, r2, offset) = try Self.parse(data: data) } + } + + public struct BranchGeS: BranchInstructionBase2 { + public static var opcode: UInt8 { 43 } + typealias Compare = CompareGe + + var r1: Registers.Index + var r2: Registers.Index + var offset: UInt32 + public init(data: Data) throws { (r1, r2, offset) = try Self.parse(data: data) } + } + + // MARK: Instruction with Arguments of Two Registers and Two Immediates (5.11) + + public struct LoadImmJumpInd: Instruction { + public static var opcode: UInt8 { 10 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let value: UInt32 + public let offset: UInt32 + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + (value, offset) = try Instructions.decodeImmediate2(data[relative: 1...], divideBy: 1, minus: 2) + } + + public func _executeImpl(state: VMState) throws -> ExecOutcome { + state.writeRegister(ra, value) + return .continued + } + + public func updatePC(state: VMState, skip _: UInt32) -> ExecOutcome { + let rbVal = state.readRegister(rb) + return Instructions.djump(state: state, target: rbVal &+ offset) + } + } + + // MARK: Instructions with Arguments of Three Registers (5.12) + + public struct Add: Instruction { + public static var opcode: UInt8 { 8 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal &+ rbVal) + return .continued + } + } + + public struct Sub: Instruction { + public static var opcode: UInt8 { 20 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal &- rbVal) + return .continued + } + } + + public struct And: Instruction { + public static var opcode: UInt8 { 23 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal & rbVal) + return .continued + } + } + + public struct Xor: Instruction { + public static var opcode: UInt8 { 28 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal ^ rbVal) + return .continued + } + } + + public struct Or: Instruction { + public static var opcode: UInt8 { 12 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal | rbVal) + return .continued + } + } + + public struct Mul: Instruction { + public static var opcode: UInt8 { 34 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal &* rbVal) + return .continued + } + } + + public struct MulUpperSS: Instruction { + public static var opcode: UInt8 { 67 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, UInt32(bitPattern: Int32((Int64(Int32(bitPattern: raVal)) * Int64(Int32(bitPattern: rbVal))) >> 32))) + return .continued + } + } + + public struct MulUpperUU: Instruction { + public static var opcode: UInt8 { 57 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, UInt32((UInt64(raVal) * UInt64(rbVal)) >> 32)) + return .continued + } + } + + public struct MulUpperSU: Instruction { + public static var opcode: UInt8 { 81 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, UInt32(bitPattern: Int32((Int64(Int32(bitPattern: raVal)) * Int64(Int32(bitPattern: rbVal))) >> 32))) + return .continued + } + } + + public struct DivU: Instruction { + public static var opcode: UInt8 { 68 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + if rbVal == 0 { + state.writeRegister(rd, UInt32.max) + } else { + state.writeRegister(rd, raVal / rbVal) + } + return .continued + } + } + + public struct DivS: Instruction { + public static var opcode: UInt8 { 64 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + if rbVal == 0 { + state.writeRegister(rd, UInt32.max) + } else if Int32(bitPattern: raVal) == Int32.min, Int32(bitPattern: rbVal) == -1 { + state.writeRegister(rd, raVal) + } else { + state.writeRegister(rd, UInt32(bitPattern: Int32(bitPattern: raVal) / Int32(bitPattern: rbVal))) + } + return .continued + } + } + + public struct RemU: Instruction { + public static var opcode: UInt8 { 73 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + if rbVal == 0 { + state.writeRegister(rd, raVal) + } else { + state.writeRegister(rd, raVal % rbVal) + } + return .continued + } + } + + public struct RemS: Instruction { + public static var opcode: UInt8 { 70 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + if rbVal == 0 { + state.writeRegister(rd, raVal) + } else if Int32(bitPattern: raVal) == Int32.min, Int32(bitPattern: rbVal) == -1 { + state.writeRegister(rd, 0) + } else { + state.writeRegister(rd, UInt32(bitPattern: Int32(bitPattern: raVal) % Int32(bitPattern: rbVal))) + } + return .continued + } + } + + public struct SetLtU: Instruction { + public static var opcode: UInt8 { 36 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, raVal < rbVal ? 1 : 0) + return .continued + } + } + + public struct SetLtS: Instruction { + public static var opcode: UInt8 { 58 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + state.writeRegister(rd, Int32(bitPattern: raVal) < Int32(bitPattern: rbVal) ? 1 : 0) + return .continued + } + } + + public struct ShloL: Instruction { + public static var opcode: UInt8 { 55 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + let shift = rbVal & 0x20 + state.writeRegister(rd, UInt32(truncatingIfNeeded: raVal << shift)) + return .continued + } + } + + public struct ShloR: Instruction { + public static var opcode: UInt8 { 51 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + let shift = rbVal & 0x20 + state.writeRegister(rd, raVal >> shift) + return .continued + } + } + + public struct SharR: Instruction { + public static var opcode: UInt8 { 77 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + let shift = rbVal & 0x20 + state.writeRegister(rd, UInt32(bitPattern: Int32(bitPattern: raVal) >> shift)) + return .continued + } + } + + public struct CmovIz: Instruction { + public static var opcode: UInt8 { 83 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + if rbVal == 0 { + state.writeRegister(rd, raVal) + } + return .continued + } + } + + public struct CmovNz: Instruction { + public static var opcode: UInt8 { 84 } + + public let ra: Registers.Index + public let rb: Registers.Index + public let rd: Registers.Index + + public init(data: Data) throws { + ra = try Registers.Index(ra: data.at(relative: 0)) + rb = try Registers.Index(rb: data.at(relative: 0)) + rd = try Registers.Index(rd: data.at(relative: 1)) + } + + public func _executeImpl(state: VMState) -> ExecOutcome { + let raVal = state.readRegister(ra) + let rbVal = state.readRegister(rb) + if rbVal != 0 { + state.writeRegister(rd, raVal) + } + return .continued + } + } } // MARK: Branch Helpers @@ -1192,9 +1762,7 @@ protocol BranchInstructionBase: Instruction { var offset: UInt32 { get set } func _executeImpl(state _: VMState) throws -> ExecOutcome - func updatePC(state: VMState, skip: UInt32) -> ExecOutcome - func condition(state: VMState) -> Bool } @@ -1225,6 +1793,48 @@ extension BranchInstructionBase { } } +// for branch in A.5.10 +protocol BranchInstructionBase2: Instruction { + associatedtype Compare: BranchCompare + + var r1: Registers.Index { get set } + var r2: Registers.Index { get set } + var offset: UInt32 { get set } + + func _executeImpl(state _: VMState) throws -> ExecOutcome + func updatePC(state: VMState, skip: UInt32) -> ExecOutcome + func condition(state: VMState) -> Bool +} + +extension BranchInstructionBase2 { + public static func parse(data: Data) throws -> (Registers.Index, Registers.Index, UInt32) { + let offset = try Instructions.decodeImmediate(data.at(relative: 1...)) + let r1 = try Registers.Index(ra: data.at(relative: 0)) + let r2 = try Registers.Index(rb: data.at(relative: 0)) + return (r1, r2, offset) + } + + public func _executeImpl(state _: VMState) throws -> ExecOutcome { .continued } + + public func updatePC(state: VMState, skip: UInt32) -> ExecOutcome { + guard Instructions.isBranchValid(state: state, offset: offset) else { + return .exit(.panic(.invalidBranch)) + } + if condition(state: state) { + state.increasePC(offset) + } else { + state.increasePC(skip + 1) + } + return .continued + } + + public func condition(state: VMState) -> Bool { + let r1Val = state.readRegister(r1) + let r2Val = state.readRegister(r2) + return Compare.compare(a: r1Val, b: r2Val) + } +} + public struct CompareEq: BranchCompare { public static func compare(a: UInt32, b: UInt32) -> Bool { Int32(bitPattern: a) == Int32(bitPattern: b) diff --git a/PolkaVM/Sources/PolkaVM/Registers.swift b/PolkaVM/Sources/PolkaVM/Registers.swift index dd9a047f..91180624 100644 --- a/PolkaVM/Sources/PolkaVM/Registers.swift +++ b/PolkaVM/Sources/PolkaVM/Registers.swift @@ -8,6 +8,10 @@ public struct Registers: Equatable { public init(rb: UInt8) { value = min(rb >> 4, 12) } + + public init(rd: UInt8) { + value = min(rd, 12) + } } public var reg1: UInt32 = 0