Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guaranteeing #103

Merged
merged 2 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions Blockchain/Sources/Blockchain/Guaranteeing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import Codec
import Utils

public enum GuaranteeingError: Error {
case invalidGuaranteeSignature
case invalidGuaranteeCore
case coreNotAvailable
case invalidReportAuthorizer
case invalidServiceIndex
case outOfGas
case invalidContext
case duplicatedWorkPackage
case prerequistieNotFound
case invalidResultCodeHash
}

public protocol Guaranteeing {
var entropyPool: EntropyPool { get }
var timeslot: TimeslotIndex { get }
var currentValidators: ConfigFixedSizeArray<
ValidatorKey, ProtocolConfig.TotalNumberOfValidators
> { get }
var previousValidators: ConfigFixedSizeArray<
ValidatorKey, ProtocolConfig.TotalNumberOfValidators
> { get }
var reports: ConfigFixedSizeArray<
ReportItem?,
ProtocolConfig.TotalNumberOfCores
> { get }
var coreAuthorizationPool: ConfigFixedSizeArray<
ConfigLimitedSizeArray<
Data32,
ProtocolConfig.Int0,
ProtocolConfig.MaxAuthorizationsPoolItems
>,
ProtocolConfig.TotalNumberOfCores
> { get }
var serviceAccounts: [ServiceIndex: ServiceAccount] { get }
var recentHistory: RecentHistory { get }
var offenders: Set<Ed25519PublicKey> { get }
}

extension Guaranteeing {
private func withoutOffenders(keys: [Ed25519PublicKey]) -> [Ed25519PublicKey] {
keys.map { key in
if offenders.contains(key) {
Data32()
} else {
key
}
}
}

private func toCoreAssignment(_ source: [UInt32], n: UInt32, max: UInt32) -> [CoreIndex] {
source.map { CoreIndex(($0 + n) % max) }
}

private func getCoreAssignment(config: ProtocolConfigRef, randomness: Data32, timeslot: TimeslotIndex) -> [CoreIndex] {
var source = Array(repeating: UInt32(0), count: config.value.totalNumberOfValidators)
for i in 0 ..< config.value.totalNumberOfValidators {
source[i] = UInt32(config.value.totalNumberOfCores * i / config.value.totalNumberOfValidators)
}
source.shuffle(randomness: randomness)

let n = timeslot % UInt32(config.value.epochLength) / UInt32(config.value.coreAssignmentRotationPeriod)

return toCoreAssignment(source, n: n, max: UInt32(config.value.totalNumberOfCores))
}

public func update(
config: ProtocolConfigRef,
extrinsic: ExtrinsicGuarantees
) throws(GuaranteeingError) -> ConfigFixedSizeArray<
ReportItem?,
ProtocolConfig.TotalNumberOfCores
> {
let coreAssignmentRotationPeriod = UInt32(config.value.coreAssignmentRotationPeriod)

let currentCoreAssignment = getCoreAssignment(config: config, randomness: entropyPool.t2, timeslot: timeslot)
let currentCoreKeys = withoutOffenders(keys: currentValidators.map(\.ed25519))

let isEpochChanging = (timeslot % UInt32(config.value.epochLength)) < coreAssignmentRotationPeriod
let previousRandomness = isEpochChanging ? entropyPool.t3 : entropyPool.t2
let previousValidators = isEpochChanging ? previousValidators : currentValidators

let previousCoreAssignment = getCoreAssignment(
config: config,
randomness: previousRandomness,
timeslot: timeslot - coreAssignmentRotationPeriod
)
let pareviousCoreKeys = withoutOffenders(keys: previousValidators.map(\.ed25519))

var workReportHashes = Set<Data32>()

var totalMinGasRequirement: Gas = 0

for guarantee in extrinsic.guarantees {
let report = guarantee.workReport

for credential in guarantee.credential {
let isCurrent = (guarantee.timeslot / coreAssignmentRotationPeriod) == (timeslot / coreAssignmentRotationPeriod)
let keys = isCurrent ? currentCoreKeys : pareviousCoreKeys
let key = keys[Int(credential.index)]
let reportHash = report.hash()
workReportHashes.insert(reportHash)
let payload = SigningContext.guarantee + reportHash.data
guard Ed25519.verify(signature: credential.signature, message: payload, publicKey: key) else {
throw .invalidGuaranteeSignature
}

let coreAssignment = isCurrent ? currentCoreAssignment : previousCoreAssignment
guard coreAssignment[Int(credential.index)] == report.coreIndex else { // TODO: it should accepts the last core index?
throw .invalidGuaranteeCore
}
}

let coreIndex = Int(report.coreIndex)

guard reports[coreIndex] == nil ||
timeslot >= (guarantee.timeslot + UInt32(config.value.preimageReplacementPeriod))
else {
throw .coreNotAvailable
}

guard coreAuthorizationPool[coreIndex].contains(report.authorizerHash) else {
throw .invalidReportAuthorizer
}

for result in report.results {
guard let acc = serviceAccounts[result.serviceIndex] else {
throw .invalidServiceIndex
}

guard acc.codeHash == result.codeHash else {
throw .invalidResultCodeHash
}

totalMinGasRequirement += acc.minAccumlateGas
}
}

guard totalMinGasRequirement <= config.value.coreAccumulationGas else {
throw .outOfGas
}

let allRecentWorkReportHashes = Set(recentHistory.items.flatMap(\.workReportHashes.array))
guard allRecentWorkReportHashes.isDisjoint(with: workReportHashes) else {
throw .duplicatedWorkPackage
}

let contexts = Set(extrinsic.guarantees.map(\.workReport.refinementContext))

for context in contexts {
let history = recentHistory.items.first { $0.headerHash == context.anchor.headerHash }
guard let history else {
throw .invalidContext
}
guard context.anchor.stateRoot == history.stateRoot else {
throw .invalidContext
}
guard context.anchor.beefyRoot == history.mmr.hash() else {
throw .invalidContext
}
guard context.lokupAnchor.timeslot >= timeslot - UInt32(config.value.maxLookupAnchorAge) else {
throw .invalidContext
}

if let prerequistieWorkPackage = context.prerequistieWorkPackage {
guard allRecentWorkReportHashes.contains(prerequistieWorkPackage) ||
workReportHashes.contains(prerequistieWorkPackage)
else {
throw .prerequistieNotFound
}
}
}

var newReports = reports

for guarantee in extrinsic.guarantees {
let report = guarantee.workReport
let coreIndex = Int(report.coreIndex)
newReports[coreIndex] = ReportItem(
workReport: report,
timeslot: timeslot
)
}

return newReports
}
}
7 changes: 5 additions & 2 deletions Blockchain/Sources/Blockchain/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ public final class Runtime {
var newState = prevState.value

do {
try updateRecentHistory(block: block, state: &newState)

try updateSafrole(block: block, state: &newState)

try updateDisputes(block: block, state: &newState)
Expand All @@ -113,6 +111,9 @@ public final class Runtime {
newState.activityStatistics = try updateValidatorActivityStatistics(
block: block, state: prevState
)

// after reports as it need old recent history
try updateRecentHistory(block: block, state: &newState)
} catch let error as Error {
throw error
} catch let error as SafroleError {
Expand Down Expand Up @@ -227,6 +228,8 @@ public final class Runtime {
newState.reports[idx] = nil // remove available report from pending reports
}
}

newState.reports = try newState.update(config: config, extrinsic: block.extrinsic.reports)
}

// TODO: add tests
Expand Down
13 changes: 13 additions & 0 deletions Blockchain/Sources/Blockchain/Types/ExtrinsicGuarantees.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ extension ExtrinsicGuarantees: Validate {
public enum Error: Swift.Error {
case guaranteesNotSorted
case invalidCoreIndex
case invalidValidatorIndex
case credentialsNotSorted
case duplicatedWorkPackageHash
}

public func validate(config: Config) throws {
Expand All @@ -92,6 +94,17 @@ extension ExtrinsicGuarantees: Validate {
guard guarantee.credential.isSortedAndUnique(by: { $0.index < $1.index }) else {
throw Error.credentialsNotSorted
}

for credential in guarantee.credential {
guard credential.index < UInt32(config.value.totalNumberOfValidators) else {
throw Error.invalidValidatorIndex
}
}
}

let workPackageHashes = Set(guarantees.map(\.workReport.packageSpecification.workPackageHash))
guard workPackageHashes.count == guarantees.count else {
throw Error.duplicatedWorkPackageHash
}
}
}
6 changes: 3 additions & 3 deletions Blockchain/Sources/Blockchain/Types/RefinementContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Utils

// A refinement context, denoted by the set X, describes the context of the chain
// at the point that the report’s corresponding work-package was evaluated.
public struct RefinementContext: Sendable, Equatable, Codable {
public struct Anchor: Sendable, Equatable, Codable {
public struct RefinementContext: Sendable, Equatable, Codable, Hashable {
public struct Anchor: Sendable, Equatable, Codable, Hashable {
// a
public var headerHash: Data32
// s
Expand All @@ -23,7 +23,7 @@ public struct RefinementContext: Sendable, Equatable, Codable {
}
}

public struct LokupAnchor: Sendable, Equatable, Codable {
public struct LokupAnchor: Sendable, Equatable, Codable, Hashable {
// l
public var headerHash: Data32
// t
Expand Down
16 changes: 8 additions & 8 deletions Blockchain/Sources/Blockchain/Types/ServiceAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ public struct ServiceAccount: Sendable, Equatable, Codable {
public var balance: Balance

// g
public var accumlateGasLimit: Gas
public var minAccumlateGas: Gas

// m
public var onTransferGasLimit: Gas
public var minOnTransferGas: Gas

public init(
storage: [Data32: Data],
preimages: [Data32: Data],
preimageInfos: [HashAndLength: LimitedSizeArray<TimeslotIndex, ConstInt0, ConstInt3>],
codeHash: Data32,
balance: Balance,
accumlateGasLimit: Gas,
onTransferGasLimit: Gas
minAccumlateGas: Gas,
minOnTransferGas: Gas
) {
self.storage = storage
self.preimages = preimages
self.preimageInfos = preimageInfos
self.codeHash = codeHash
self.balance = balance
self.accumlateGasLimit = accumlateGasLimit
self.onTransferGasLimit = onTransferGasLimit
self.minAccumlateGas = minAccumlateGas
self.minOnTransferGas = minOnTransferGas
}
}

Expand All @@ -53,8 +53,8 @@ extension ServiceAccount: Dummy {
preimageInfos: [:],
codeHash: Data32(),
balance: 0,
accumlateGasLimit: 0,
onTransferGasLimit: 0
minAccumlateGas: 0,
minOnTransferGas: 0
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions Blockchain/Sources/Blockchain/Types/State+Merklization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ extension State {
}

private func encode(_ account: ServiceAccount) throws -> Data {
let capacity = 32 + 8 * 4 + 4 // codeHash, balance, accumlateGasLimit, onTransferGasLimit, totalByteLength, itemsCount
let capacity = 32 + 8 * 4 + 4 // codeHash, balance, minAccumlateGas, minOnTransferGas, totalByteLength, itemsCount

let encoder = JamEncoder(capacity: capacity)

try encoder.encode(account.codeHash)
try encoder.encode(account.balance)
try encoder.encode(account.accumlateGasLimit)
try encoder.encode(account.onTransferGasLimit)
try encoder.encode(account.minAccumlateGas)
try encoder.encode(account.minOnTransferGas)

// derived values
try encoder.encode(account.totalByteLength)
Expand Down
6 changes: 6 additions & 0 deletions Blockchain/Sources/Blockchain/Types/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,9 @@ extension State: Disputes {
reports = postState.reports
}
}

extension State: Guaranteeing {
public var offenders: Set<Ed25519PublicKey> {
judgements.punishSet
}
}
6 changes: 5 additions & 1 deletion Blockchain/Sources/Blockchain/Types/WorkReport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,15 @@ extension WorkReport: EncodedSize {
extension WorkReport: Validate {
public enum WorkReportError: Swift.Error {
case tooBig
case invalidCoreIndex
}

public func validate(config: Config) throws(WorkReportError) {
guard encodedSize <= config.value.maxEncodedWorkReportSize else {
throw WorkReportError.tooBig
throw .tooBig
}
guard coreIndex < UInt32(config.value.totalNumberOfCores) else {
throw .invalidCoreIndex
}
}
}
Loading
Loading