From 7c89cf5d4a067486055858082612c96b224c6e01 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Fri, 20 Dec 2024 16:46:06 +1300 Subject: [PATCH] disputests tests --- .../RuntimeProtocols/Disputes.swift | 67 ++++++++++----- .../Blockchain/RuntimeProtocols/Runtime.swift | 6 +- .../Sources/Blockchain/State/State.swift | 2 +- JAMTests/Tests/JAMTests/DisputesTests.swift | 84 +++++++++++++++++++ 4 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 JAMTests/Tests/JAMTests/DisputesTests.swift diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift index 3dc1d9f7..06ab034e 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift @@ -1,6 +1,6 @@ import Utils -public enum DisputeError: Error { +public enum DisputesError: Error { case invalidEpoch case invalidValidatorIndex case invalidJudgementSignature @@ -11,6 +11,8 @@ public enum DisputeError: Error { case expectInFaults case expectInCulprits case invalidPublicKey + case invalidFaults + case invalidCulprit } public struct ReportItem: Sendable, Equatable, Codable { @@ -30,7 +32,7 @@ extension ReportItem: Validate { public typealias Config = ProtocolConfigRef } -public struct DisputePostState: Sendable, Equatable { +public struct DisputesPostState: Sendable, Equatable { public var judgements: JudgementsState public var reports: ConfigFixedSizeArray< ReportItem?, @@ -63,17 +65,17 @@ public protocol Disputes { ValidatorKey, ProtocolConfig.TotalNumberOfValidators > { get } - func update(config: ProtocolConfigRef, disputes: ExtrinsicDisputes) throws(DisputeError) -> ( - state: DisputePostState, + func update(config: ProtocolConfigRef, disputes: ExtrinsicDisputes) throws(DisputesError) -> ( + state: DisputesPostState, offenders: [Ed25519PublicKey] ) - mutating func mergeWith(postState: DisputePostState) + mutating func mergeWith(postState: DisputesPostState) } extension Disputes { - public func update(config: ProtocolConfigRef, disputes: ExtrinsicDisputes) throws(DisputeError) -> ( - state: DisputePostState, + public func update(config: ProtocolConfigRef, disputes: ExtrinsicDisputes) throws(DisputesError) -> ( + state: DisputesPostState, offenders: [Ed25519PublicKey] ) { var newJudgements = judgements @@ -101,7 +103,7 @@ extension Disputes { let prefix = judgement.isValid ? SigningContext.valid : SigningContext.invalid let payload = prefix + verdict.reportHash.data let pubkey = try Result { try Ed25519.PublicKey(from: signer) } - .mapError { _ in DisputeError.invalidPublicKey } + .mapError { _ in DisputesError.invalidPublicKey } .get() guard pubkey.verify(signature: judgement.signature, message: payload) else { throw .invalidJudgementSignature @@ -151,23 +153,39 @@ extension Disputes { let two_third_plus_one_validators = config.value.totalNumberOfValidators * 2 / 3 + 1 for (hash, vote) in votes { if vote == 0 { - // any verdict containing solely valid judgements - // implies the same report having at least one valid entry in the faults sequence f - guard disputes.faults.contains(where: { $0.reportHash == hash }) else { - throw .expectInFaults + // Any verdict containing solely invalid judgements + // implies the same report having at least two valid entries in the culprits sequence c + let culprits = disputes.culprits.filter { $0.reportHash == hash } + guard culprits.count >= 2 else { + throw .expectInCulprits } tobeRemoved.insert(hash) newJudgements.banSet.insert(hash) + + let faults = disputes.faults.filter { $0.reportHash == hash } + for fault in faults { + // check faults are indeed invalid + guard fault.vote else { + throw .invalidFaults + } + } } else if vote == third_validators { // wonky tobeRemoved.insert(hash) newJudgements.wonkySet.insert(hash) } else if vote == two_third_plus_one_validators { - // Any verdict containing solely invalid judgements - // implies the same report having at least two valid entries in the culprits sequence c - guard disputes.culprits.count(where: { $0.reportHash == hash }) >= 2 else { - throw .expectInCulprits + // any verdict containing solely valid judgements + // implies the same report having at least one valid entry in the faults sequence f + let faults = disputes.faults.filter { $0.reportHash == hash } + guard faults.count >= 1 else { + throw .expectInFaults + } + for fault in faults { + // check faults are indeed invalid + guard !fault.vote else { + throw .invalidFaults + } } newJudgements.goodSet.insert(hash) @@ -176,6 +194,12 @@ extension Disputes { } } + for culprit in disputes.culprits { + guard newJudgements.banSet.contains(culprit.reportHash) else { + throw .invalidCulprit + } + } + for i in 0 ..< newReports.count { if let report = newReports[i]?.workReport { let hash = report.hash() @@ -185,9 +209,12 @@ extension Disputes { } } - return (state: DisputePostState( - judgements: newJudgements, - reports: newReports - ), offenders: offenders) + return ( + state: DisputesPostState( + judgements: newJudgements, + reports: newReports + ), + offenders: offenders + ) } } diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift index bb9a7889..4f6c6fc0 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift @@ -9,7 +9,7 @@ private let logger = Logger(label: "Runtime") public final class Runtime { public enum Error: Swift.Error { case safroleError(SafroleError) - case DisputeError(DisputeError) + case disputesError(DisputesError) case invalidTimeslot(got: TimeslotIndex, context: TimeslotIndex) case invalidReportAuthorizer case encodeError(any Swift.Error) @@ -195,8 +195,8 @@ public final class Runtime { throw error } catch let error as SafroleError { throw .safroleError(error) - } catch let error as DisputeError { - throw .DisputeError(error) + } catch let error as DisputesError { + throw .disputesError(error) } catch { throw .other(error) } diff --git a/Blockchain/Sources/Blockchain/State/State.swift b/Blockchain/Sources/Blockchain/State/State.swift index 531169d8..836e79dd 100644 --- a/Blockchain/Sources/Blockchain/State/State.swift +++ b/Blockchain/Sources/Blockchain/State/State.swift @@ -447,7 +447,7 @@ extension State: Safrole { extension State: Assurances {} extension State: Disputes { - public mutating func mergeWith(postState: DisputePostState) { + public mutating func mergeWith(postState: DisputesPostState) { judgements = postState.judgements reports = postState.reports } diff --git a/JAMTests/Tests/JAMTests/DisputesTests.swift b/JAMTests/Tests/JAMTests/DisputesTests.swift new file mode 100644 index 00000000..16ca3d86 --- /dev/null +++ b/JAMTests/Tests/JAMTests/DisputesTests.swift @@ -0,0 +1,84 @@ +import Blockchain +import Codec +import Foundation +import Testing +import Utils + +@testable import JAMTests + +struct DisputesState: Equatable, Codable, Disputes { + var judgements: JudgementsState + var reports: ConfigFixedSizeArray< + ReportItem?, + ProtocolConfig.TotalNumberOfCores + > + var timeslot: TimeslotIndex + var currentValidators: ConfigFixedSizeArray< + ValidatorKey, ProtocolConfig.TotalNumberOfValidators + > + var previousValidators: ConfigFixedSizeArray< + ValidatorKey, ProtocolConfig.TotalNumberOfValidators + > + + mutating func mergeWith(postState: DisputesPostState) { + judgements = postState.judgements + reports = postState.reports + } +} + +struct DisputesTestcase: Codable { + var input: ExtrinsicDisputes + var preState: DisputesState + var output: Either<[Ed25519PublicKey], UInt8> + var postState: DisputesState +} + +struct DisputesTests { + static func loadTests(variant: TestVariants) throws -> [Testcase] { + try TestLoader.getTestcases(path: "disputes/\(variant)", extension: "bin") + } + + func disputesTests(_ testcase: Testcase, variant: TestVariants) throws { + let config = variant.config + let decoder = JamDecoder(data: testcase.data, config: config) + let testcase = try decoder.decode(DisputesTestcase.self) + + var state = testcase.preState + let result = Result { + try testcase.input.validate(config: config) + return try state.update( + config: config, + disputes: testcase.input + ) + } + switch result { + case let .success((postState, offenders)): + switch testcase.output { + case let .left(output): + state.mergeWith(postState: postState) + #expect(state == testcase.postState) + #expect(offenders == output) + 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 DisputesTests.loadTests(variant: .tiny)) + func tinyTests(_ testcase: Testcase) throws { + try disputesTests(testcase, variant: .tiny) + } + + @Test(arguments: try DisputesTests.loadTests(variant: .full)) + func fullTests(_ testcase: Testcase) throws { + try disputesTests(testcase, variant: .full) + } +}