From c8deca877e63b333cec7c7b594175d9d6a43f989 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Thu, 19 Dec 2024 20:49:14 +1300 Subject: [PATCH] assurances tests --- .../RuntimeProtocols/Assurances.swift | 87 ++++++++++++++++++ .../Blockchain/RuntimeProtocols/Runtime.swift | 38 ++------ .../Sources/Blockchain/State/State.swift | 2 + .../Types/ExtrinsicAvailability.swift | 2 +- JAMTests/Tests/JAMTests/AssurancesTests.swift | 91 +++++++++++++++++++ .../Tests/JAMTests/RecentHistoryTests.swift | 4 +- Utils/Sources/Utils/ConfigSizeBitString.swift | 6 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- 8 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 Blockchain/Sources/Blockchain/RuntimeProtocols/Assurances.swift create mode 100644 JAMTests/Tests/JAMTests/AssurancesTests.swift diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Assurances.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Assurances.swift new file mode 100644 index 00000000..7d701060 --- /dev/null +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Assurances.swift @@ -0,0 +1,87 @@ +import Utils + +public enum AssurancesError: Error { + case invalidAssuranceSignature + case assuranceForEmptyCore + case invalidAssuranceParentHash +} + +public protocol Assurances { + var reports: + ConfigFixedSizeArray< + ReportItem?, + ProtocolConfig.TotalNumberOfCores + > + { get } + + var currentValidators: + ConfigFixedSizeArray< + ValidatorKey, + ProtocolConfig.TotalNumberOfValidators + > + { get } +} + +extension Assurances { + public func update( + config: ProtocolConfigRef, + timeslot: TimeslotIndex, + extrinsic: ExtrinsicAvailability, + parentHash: Data32 + ) throws -> ( + newReports: ConfigFixedSizeArray< + ReportItem?, + ProtocolConfig.TotalNumberOfCores + >, + availableReports: [WorkReport] + ) { + var newReports = reports + + for i in 0 ..< newReports.count { + if let report = newReports[i] { + if (report.timeslot + UInt32(config.value.preimageReplacementPeriod)) <= timeslot { + newReports[i] = nil + } + } + } + + for assurance in extrinsic.assurances { + guard assurance.parentHash == parentHash else { + throw AssurancesError.invalidAssuranceParentHash + } + + let hash = Blake2b256.hash(assurance.parentHash, assurance.assurance) + let payload = SigningContext.available + hash.data + let validatorKey = try currentValidators.at(Int(assurance.validatorIndex)) + let pubkey = try Ed25519.PublicKey(from: validatorKey.ed25519) + guard pubkey.verify(signature: assurance.signature, message: payload) else { + throw AssurancesError.invalidAssuranceSignature + } + } + + var availabilityCount = Array(repeating: 0, count: config.value.totalNumberOfCores) + for assurance in extrinsic.assurances { + for (coreIdx, bit) in assurance.assurance.enumerated() where bit { + // ExtrinsicAvailability.validate() ensures that validatorIndex is in range + availabilityCount[coreIdx] += 1 + } + } + + var availableReports = [WorkReport]() + + for (idx, count) in availabilityCount.enumerated() where count > 0 { + guard let report = reports[idx] else { + throw AssurancesError.assuranceForEmptyCore + } + if count >= ProtocolConfig.TwoThirdValidatorsPlusOne.read(config: config) { + availableReports.append(report.workReport) + newReports[idx] = nil // remove available report from pending reports + } + } + + return ( + newReports: newReports, + availableReports: availableReports + ) + } +} diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift index 65dc51bb..bb9a7889 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift @@ -343,36 +343,16 @@ public final class Runtime { // returns available reports public func updateReports(block: BlockRef, state newState: inout State) throws -> [WorkReport] { - for assurance in block.extrinsic.availability.assurances { - let hash = Blake2b256.hash(assurance.parentHash, assurance.assurance) - let payload = SigningContext.available + hash.data - let validatorKey = try newState.currentValidators.at(Int(assurance.validatorIndex)) - let pubkey = try Ed25519.PublicKey(from: validatorKey.ed25519) - guard pubkey.verify(signature: assurance.signature, message: payload) else { - throw Error.invalidAssuranceSignature - } - } - - var availabilityCount = Array(repeating: 0, count: config.value.totalNumberOfCores) - for assurance in block.extrinsic.availability.assurances { - for bit in assurance.assurance where bit { - // ExtrinsicAvailability.validate() ensures that validatorIndex is in range - availabilityCount[Int(assurance.validatorIndex)] += 1 - } - } - - var availableReports = [WorkReport]() - - for (idx, count) in availabilityCount.enumerated() where count > 0 { - guard let report = newState.reports[idx] else { - throw Error.assuranceForEmptyCore - } - if count >= ProtocolConfig.TwoThirdValidatorsPlusOne.read(config: config) { - availableReports.append(report.workReport) - newState.reports[idx] = nil // remove available report from pending reports - } - } + let ( + newReports: newReports, availableReports: availableReports + ) = try newState.update( + config: config, + timeslot: block.header.timeslot, + extrinsic: block.extrinsic.availability, + parentHash: block.header.parentHash + ) + newState.reports = newReports newState.reports = try newState.update(config: config, extrinsic: block.extrinsic.reports) return availableReports diff --git a/Blockchain/Sources/Blockchain/State/State.swift b/Blockchain/Sources/Blockchain/State/State.swift index d3d69ccc..531169d8 100644 --- a/Blockchain/Sources/Blockchain/State/State.swift +++ b/Blockchain/Sources/Blockchain/State/State.swift @@ -444,6 +444,8 @@ extension State: Safrole { } } +extension State: Assurances {} + extension State: Disputes { public mutating func mergeWith(postState: DisputePostState) { judgements = postState.judgements diff --git a/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift b/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift index 2b179853..b7c97b6c 100644 --- a/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift +++ b/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift @@ -58,7 +58,7 @@ extension ExtrinsicAvailability: Validate { throw .assurancesNotSorted } for assurance in assurances { - guard assurance.validatorIndex < UInt32(config.value.totalNumberOfCores) else { + guard assurance.validatorIndex < UInt32(config.value.totalNumberOfValidators) else { throw .invalidValidatorIndex } } diff --git a/JAMTests/Tests/JAMTests/AssurancesTests.swift b/JAMTests/Tests/JAMTests/AssurancesTests.swift new file mode 100644 index 00000000..7f8446e0 --- /dev/null +++ b/JAMTests/Tests/JAMTests/AssurancesTests.swift @@ -0,0 +1,91 @@ +import Blockchain +import Codec +import Foundation +import Testing +import Utils + +@testable import JAMTests + +struct AssurancesInput: Codable { + var assurances: ExtrinsicAvailability + var timeslot: TimeslotIndex + var parentHash: Data32 +} + +struct AssuranceState: Equatable, Codable, Assurances { + var reports: ConfigFixedSizeArray + var currentValidators: + ConfigFixedSizeArray +} + +struct AssurancesTestcase: Codable { + var input: AssurancesInput + var preState: AssuranceState + var output: Either<[WorkReport], UInt8> + var postState: AssuranceState +} + +enum AssurancesTestVariants: String, CaseIterable { + case tiny + case full + + var config: ProtocolConfigRef { + switch self { + case .tiny: + ProtocolConfigRef.tiny + case .full: + ProtocolConfigRef.mainnet + } + } +} + +struct AssurancesTests { + static func loadTests(variant: AssurancesTestVariants) throws -> [Testcase] { + try TestLoader.getTestcases(path: "assurances/\(variant)", extension: "bin") + } + + func assurancesTests(_ testcase: Testcase, variant: AssurancesTestVariants) throws { + let config = variant.config + let decoder = JamDecoder(data: testcase.data, config: config) + let testcase = try decoder.decode(AssurancesTestcase.self) + + var state = testcase.preState + let result = Result { + try testcase.input.assurances.validate(config: config) + return try state.update( + config: config, timeslot: testcase.input.timeslot, + extrinsic: testcase.input.assurances, + parentHash: testcase.input.parentHash + ) + } + switch result { + case let .success((newReports, availableReports)): + switch testcase.output { + case let .left(reports): + state.reports = newReports + #expect(state == testcase.postState) + #expect(availableReports == reports) + case .right: + Issue.record("Expected error, got \(result)") + } + case .failure: + switch testcase.output { + case .left: + Issue.record("Expected success, got \(result)") + case .right: + // ignore error code because it is unspecified + break + } + } + } + + @Test(arguments: try AssurancesTests.loadTests(variant: .tiny)) + func tinyTests(_ testcase: Testcase) throws { + try assurancesTests(testcase, variant: .tiny) + } + + @Test(arguments: try AssurancesTests.loadTests(variant: .full)) + func fullTests(_ testcase: Testcase) throws { + try assurancesTests(testcase, variant: .full) + } +} diff --git a/JAMTests/Tests/JAMTests/RecentHistoryTests.swift b/JAMTests/Tests/JAMTests/RecentHistoryTests.swift index ecfce568..9f17fcbe 100644 --- a/JAMTests/Tests/JAMTests/RecentHistoryTests.swift +++ b/JAMTests/Tests/JAMTests/RecentHistoryTests.swift @@ -18,7 +18,7 @@ struct RecentHistoryInput: Codable { var workPackages: [ReportedWorkPackage] } -struct RecentHisoryTestcase: Codable { +struct RecentHistoryTestcase: Codable { var input: RecentHistoryInput var preState: RecentHistory var postState: RecentHistory @@ -32,7 +32,7 @@ struct RecentHistoryTests { @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) + let testcase = try JamDecoder.decode(RecentHistoryTestcase.self, from: testcase.data, withConfig: config) var state = testcase.preState state.update( diff --git a/Utils/Sources/Utils/ConfigSizeBitString.swift b/Utils/Sources/Utils/ConfigSizeBitString.swift index 5c286efe..bba2ebd1 100644 --- a/Utils/Sources/Utils/ConfigSizeBitString.swift +++ b/Utils/Sources/Utils/ConfigSizeBitString.swift @@ -35,7 +35,7 @@ public struct ConfigSizeBitString: Equatable, Sendable, Cod private func at(unchecked index: Int) -> Bool { let byteIndex = index / 8 let bitIndex = index % 8 - return (bytes[byteIndex] & (1 << bitIndex)) != 0 + return (bytes[relative: byteIndex] & (1 << bitIndex)) != 0 } /// Formats the bitstring in binary digits. @@ -59,9 +59,9 @@ public struct ConfigSizeBitString: Equatable, Sendable, Cod let byteIndex = index / 8 let bitIndex = index % 8 if value { - bytes[byteIndex] |= (1 << bitIndex) + bytes[bytes.relative(offset: byteIndex)] |= (1 << bitIndex) } else { - bytes[byteIndex] &= ~(1 << bitIndex) + bytes[bytes.relative(offset: byteIndex)] &= ~(1 << bitIndex) } } } diff --git a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bd080355..6da8821a 100644 --- a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "8e21a58b6d7f842c098f4e974e374c6d0247726a1d3017dda720b3de822a747b", + "originHash" : "df8d708ee631530dce6fd56811e5100a965c55290ef02b21f0ffdb94c192448c", "pins" : [ { "identity" : "async-channels", @@ -247,7 +247,7 @@ { "identity" : "swift-numerics", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics.git", + "location" : "https://github.com/apple/swift-numerics", "state" : { "branch" : "main", "revision" : "e30276bff2ff5ed80566fbdca49f50aa160b0e83" @@ -292,7 +292,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", "version" : "600.0.0-prerelease-2024-06-12"