Skip to content

Commit

Permalink
Merge branch 'master' into dev_mackun
Browse files Browse the repository at this point in the history
* master:
  implements disputes protocol (#92)
  • Loading branch information
MacOMNI committed Aug 30, 2024
2 parents 9c5fbf6 + 7e0c3cd commit ccb96d4
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 88 deletions.
218 changes: 218 additions & 0 deletions Blockchain/Sources/Blockchain/Disputes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import Utils

public enum DisputeError: Error {
case invalidEpoch
case invalidValidatorIndex
case invalidJudgementSignature
case invalidCulpritSigner
case invalidCulpritSignature
case invalidFaultSigner
case invalidFaultSignature
case verdictsNotSorted
case culpritsNotSorted
case faultsNotSorted
case duplicatedReport
case judgementsNotSorted
case invalidJudgementsCount
case expectInFaults
case expectInCulprits
}

public struct ReportItem: Sendable, Equatable, Codable {
public var workReport: WorkReport
public var timeslot: TimeslotIndex

public init(
workReport: WorkReport,
timeslot: TimeslotIndex
) {
self.workReport = workReport
self.timeslot = timeslot
}
}

public struct DisputePostState: Sendable, Equatable {
public var judgements: JudgementsState
public var reports: ConfigFixedSizeArray<
ReportItem?,
ProtocolConfig.TotalNumberOfCores
>

public init(
judgements: JudgementsState,
reports: ConfigFixedSizeArray<
ReportItem?,
ProtocolConfig.TotalNumberOfCores
>
) {
self.judgements = judgements
self.reports = reports
}
}

public protocol Disputes {
var judgements: JudgementsState { get }
var reports: ConfigFixedSizeArray<
ReportItem?,
ProtocolConfig.TotalNumberOfCores
> { get }
var timeslot: TimeslotIndex { get }
var currentValidators: ConfigFixedSizeArray<
ValidatorKey, ProtocolConfig.TotalNumberOfValidators
> { get }
var previousValidators: ConfigFixedSizeArray<
ValidatorKey, ProtocolConfig.TotalNumberOfValidators
> { get }

func update(config: ProtocolConfigRef, disputes: ExtrinsicDisputes) throws(DisputeError) -> (
state: DisputePostState,
offenders: [Ed25519PublicKey]
)

mutating func mergeWith(postState: DisputePostState)
}

extension Disputes {
public func update(config: ProtocolConfigRef, disputes: ExtrinsicDisputes) throws(DisputeError) -> (
state: DisputePostState,
offenders: [Ed25519PublicKey]
) {
var newJudgements = judgements
var newReports = reports
var offenders: [Ed25519PublicKey] = []

let epochLength = UInt32(config.value.epochLength)
let currentEpoch = timeslot / epochLength
let lastEpoch = currentEpoch == 0 ? nil : currentEpoch - 1

for verdict in disputes.verdicts {
let isCurrent = verdict.epoch == currentEpoch
let isLast = verdict.epoch == lastEpoch
guard isCurrent || isLast else {
throw .invalidEpoch
}

let validators = isCurrent ? currentValidators : previousValidators

for judgement in verdict.judgements {
guard let signer = validators[safe: Int(judgement.validatorIndex)]?.ed25519 else {
throw .invalidValidatorIndex
}

let prefix = judgement.isValid ? SigningContext.valid : SigningContext.invalid
let payload = prefix + verdict.reportHash.data
guard Ed25519.verify(signature: judgement.signature, message: payload, publicKey: signer) else {
throw .invalidJudgementSignature
}
}

guard verdict.judgements.isSortedAndUnique(by: { $0.validatorIndex < $1.validatorIndex }) else {
throw .judgementsNotSorted
}
}

var validSigners = Set<Ed25519PublicKey>(currentValidators.map(\.ed25519))
validSigners.formUnion(previousValidators.map(\.ed25519))
validSigners.subtract(judgements.punishSet)

for culprit in disputes.culprits {
guard validSigners.contains(culprit.validatorKey) else {
throw .invalidCulpritSigner
}

let payload = SigningContext.guarantee + culprit.reportHash.data
guard Ed25519.verify(signature: culprit.signature, message: payload, publicKey: culprit.validatorKey) else {
throw .invalidCulpritSignature
}

newJudgements.punishSet.insert(culprit.validatorKey)
offenders.append(culprit.validatorKey)
}

for fault in disputes.faults {
guard validSigners.contains(fault.validatorKey) else {
throw .invalidFaultSigner
}

let prefix = fault.vote ? SigningContext.valid : SigningContext.invalid
let payload = prefix + fault.reportHash.data
guard Ed25519.verify(signature: fault.signature, message: payload, publicKey: fault.validatorKey) else {
throw .invalidFaultSignature
}

newJudgements.punishSet.insert(fault.validatorKey)
offenders.append(fault.validatorKey)
}

guard disputes.verdicts.isSortedAndUnique(by: { $0.reportHash < $1.reportHash }) else {
throw .verdictsNotSorted
}

guard disputes.culprits.isSortedAndUnique(by: { $0.validatorKey < $1.validatorKey }) else {
throw .culpritsNotSorted
}

guard disputes.faults.isSortedAndUnique(by: { $0.validatorKey < $1.validatorKey }) else {
throw .faultsNotSorted
}

var allReports = Set(disputes.verdicts.map(\.reportHash))
allReports.formUnion(judgements.goodSet)
allReports.formUnion(judgements.banSet)
allReports.formUnion(judgements.wonkySet)

let expectedReportCount = disputes.verdicts.count + judgements.goodSet.count + judgements.banSet.count + judgements.wonkySet.count

guard allReports.count == expectedReportCount else {
throw .duplicatedReport
}

let votes = disputes.verdicts.map {
(hash: $0.reportHash, vote: $0.judgements.reduce(into: 0) { $0 += $1.isValid ? 1 : 0 })
}

var tobeRemoved = Set<Data32>()
let third_validators = config.value.totalNumberOfValidators / 3
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
}

tobeRemoved.insert(hash)
newJudgements.banSet.insert(hash)
} 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
}

newJudgements.goodSet.insert(hash)
} else {
throw .invalidJudgementsCount
}
}

for i in 0 ..< newReports.count {
if let report = newReports[i]?.workReport {
let hash = report.hash()
if tobeRemoved.contains(hash) {
newReports[i] = nil
}
}
}

return (state: DisputePostState(
judgements: newJudgements,
reports: newReports
), offenders: offenders)
}
}
36 changes: 24 additions & 12 deletions Blockchain/Sources/Blockchain/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Utils
public final class Runtime {
public enum Error: Swift.Error {
case safroleError(SafroleError)
case DisputeError(DisputeError)
case invalidTimeslot
case invalidReportAuthorizer
case encodeError(any Swift.Error)
Expand All @@ -13,6 +14,7 @@ public final class Runtime {
case invalidHeaderStateRoot
case invalidHeaderEpochMarker
case invalidHeaderWinningTickets
case invalidHeaderOffendersMarkers
case other(any Swift.Error)
}

Expand Down Expand Up @@ -50,12 +52,11 @@ public final class Runtime {
throw Error.invalidTimeslot
}

// epoch is validated at apply time
// epoch is validated at apply time by Safrole

// winning tickets is validated at apply time
// winning tickets is validated at apply time by Safrole

// TODO: validate judgementsMarkers
// TODO: validate offendersMarkers
// offendersMarkers is validated at apply time by Disputes

// TODO: validate block.header.seal
}
Expand Down Expand Up @@ -88,13 +89,25 @@ public final class Runtime {
throw error
} catch let error as SafroleError {
throw .safroleError(error)
} catch let error as DisputeError {
throw .DisputeError(error)
} catch {
throw .other(error)
}

return StateRef(newState)
}

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,
parentStateRoot: block.header.priorStateRoot,
accumulateRoot: Data32(), // TODO: calculate accumulation result
workReportHashes: ConfigLimitedSizeArray(config: config, array: workReportHashes)
)
}

public func updateSafrole(block: BlockRef, state newState: inout State) throws {
let safroleResult = try newState.updateSafrole(
config: config,
Expand All @@ -114,14 +127,13 @@ public final class Runtime {
}
}

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,
parentStateRoot: block.header.priorStateRoot,
accumulateRoot: Data32(), // TODO: calculate accumulation result
workReportHashes: ConfigLimitedSizeArray(config: config, array: workReportHashes)
)
public func updateDisputes(block: BlockRef, state newState: inout State) throws {
let (posState, offenders) = try newState.update(config: config, disputes: block.extrinsic.judgements)
newState.mergeWith(postState: posState)

guard offenders == block.header.offendersMarkers else {
throw Error.invalidHeaderOffendersMarkers
}
}

// TODO: add tests
Expand Down
14 changes: 1 addition & 13 deletions Blockchain/Sources/Blockchain/Safrole.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,6 @@ public struct SafrolePostState: Sendable, Equatable {
self.ticketsOrKeys = ticketsOrKeys
self.ticketsVerifier = ticketsVerifier
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.timeslot == rhs.timeslot &&
lhs.entropyPool == rhs.entropyPool &&
lhs.previousValidators == rhs.previousValidators &&
lhs.currentValidators == rhs.currentValidators &&
lhs.nextValidators == rhs.nextValidators &&
lhs.validatorQueue == rhs.validatorQueue &&
lhs.ticketsAccumulator == rhs.ticketsAccumulator &&
lhs.ticketsOrKeys == rhs.ticketsOrKeys &&
lhs.ticketsVerifier == rhs.ticketsVerifier
}
}

public protocol Safrole {
Expand Down Expand Up @@ -336,7 +324,7 @@ extension Safrole {
}

let newTickets = try extrinsics.getTickets(verifier: verifier, entropy: newEntropyPool.2)
guard newTickets.isSorted() else {
guard newTickets.isSortedAndUnique() else {
throw SafroleError.extrinsicsNotSorted
}

Expand Down
8 changes: 0 additions & 8 deletions Blockchain/Sources/Blockchain/Types/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ public struct Header: Sendable, Equatable, Codable {
ProtocolConfig.EpochLength
>?

// Hj: The verdicts markers must contain exactly the sequence of report hashes of all new
// bad & wonky verdicts.
public var judgementsMarkers: [Data32]

// Ho: The offenders markers must contain exactly the sequence of keys of all new offenders.
public var offendersMarkers: [Ed25519PublicKey]

Expand All @@ -57,7 +53,6 @@ public struct Header: Sendable, Equatable, Codable {
Ticket,
ProtocolConfig.EpochLength
>?,
judgementsMarkers: [Data32],
offendersMarkers: [Ed25519PublicKey],
authorIndex: ValidatorIndex,
vrfSignature: BandersnatchSignature
Expand All @@ -68,7 +63,6 @@ public struct Header: Sendable, Equatable, Codable {
self.timeslot = timeslot
self.epoch = epoch
self.winningTickets = winningTickets
self.judgementsMarkers = judgementsMarkers
self.offendersMarkers = offendersMarkers
self.authorIndex = authorIndex
self.vrfSignature = vrfSignature
Expand Down Expand Up @@ -104,7 +98,6 @@ extension Header.Unsigned: Dummy {
timeslot: 0,
epoch: nil,
winningTickets: nil,
judgementsMarkers: [],
offendersMarkers: [],
authorIndex: 0,
vrfSignature: BandersnatchSignature()
Expand Down Expand Up @@ -138,7 +131,6 @@ extension Header {
public var timeslot: TimeslotIndex { unsigned.timeslot }
public var epoch: EpochMarker? { unsigned.epoch }
public var winningTickets: ConfigFixedSizeArray<Ticket, ProtocolConfig.EpochLength>? { unsigned.winningTickets }
public var judgementsMarkers: [Data32] { unsigned.judgementsMarkers }
public var offendersMarkers: [Ed25519PublicKey] { unsigned.offendersMarkers }
public var authorIndex: ValidatorIndex { unsigned.authorIndex }
public var vrfSignature: BandersnatchSignature { unsigned.vrfSignature }
Expand Down
Loading

0 comments on commit ccb96d4

Please sign in to comment.