Skip to content

Commit

Permalink
assurances tests
Browse files Browse the repository at this point in the history
  • Loading branch information
xlc committed Dec 19, 2024
1 parent c59725a commit c8deca8
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 38 deletions.
87 changes: 87 additions & 0 deletions Blockchain/Sources/Blockchain/RuntimeProtocols/Assurances.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
38 changes: 9 additions & 29 deletions Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Blockchain/Sources/Blockchain/State/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ extension State: Safrole {
}
}

extension State: Assurances {}

extension State: Disputes {
public mutating func mergeWith(postState: DisputePostState) {
judgements = postState.judgements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
91 changes: 91 additions & 0 deletions JAMTests/Tests/JAMTests/AssurancesTests.swift
Original file line number Diff line number Diff line change
@@ -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<ReportItem?, ProtocolConfig.TotalNumberOfCores>
var currentValidators:
ConfigFixedSizeArray<ValidatorKey, ProtocolConfig.TotalNumberOfValidators>
}

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)")

Check warning on line 69 in JAMTests/Tests/JAMTests/AssurancesTests.swift

View check run for this annotation

Codecov / codecov/patch

JAMTests/Tests/JAMTests/AssurancesTests.swift#L69

Added line #L69 was not covered by tests
}
case .failure:
switch testcase.output {
case .left:
Issue.record("Expected success, got \(result)")

Check warning on line 74 in JAMTests/Tests/JAMTests/AssurancesTests.swift

View check run for this annotation

Codecov / codecov/patch

JAMTests/Tests/JAMTests/AssurancesTests.swift#L74

Added line #L74 was not covered by tests
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)
}
}
4 changes: 2 additions & 2 deletions JAMTests/Tests/JAMTests/RecentHistoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions Utils/Sources/Utils/ConfigSizeBitString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct ConfigSizeBitString<TBitLength: ReadInt>: 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.
Expand All @@ -59,9 +59,9 @@ public struct ConfigSizeBitString<TBitLength: ReadInt>: 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)
}
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c8deca8

Please sign in to comment.