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

Preimages tests #263

Merged
merged 2 commits into from
Jan 7, 2025
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
78 changes: 78 additions & 0 deletions Blockchain/Sources/Blockchain/RuntimeProtocols/Preimages.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Foundation
import Utils

public enum PreimagesError: Error {
case preimagesNotSorted
case duplicatedPreimage
case invalidServiceIndex
}

public struct PreimageUpdate: Sendable, Equatable {
public let serviceIndex: UInt32
public let hash: Data32
public let data: Data
public let length: UInt32
public let timeslot: TimeslotIndex

public init(serviceIndex: UInt32, hash: Data32, data: Data, length: UInt32, timeslot: TimeslotIndex) {
self.serviceIndex = serviceIndex
self.hash = hash
self.data = data
self.length = length
self.timeslot = timeslot
}
}

public struct PreimagesPostState: Sendable, Equatable {
public let updates: [PreimageUpdate]

public init(updates: [PreimageUpdate]) {
self.updates = updates
}
}

public protocol Preimages {
func get(serviceAccount index: ServiceIndex, preimageHash hash: Data32) async throws -> Data?
func get(serviceAccount index: ServiceIndex, preimageHash hash: Data32, length: UInt32) async throws
-> LimitedSizeArray<TimeslotIndex, ConstInt0, ConstInt3>?

mutating func mergeWith(postState: PreimagesPostState)
}

extension Preimages {
public func updatePreimages(
config _: ProtocolConfigRef,
timeslot: TimeslotIndex,
preimages: ExtrinsicPreimages
) async throws(PreimagesError) -> PreimagesPostState {
let preimages = preimages.preimages
var updates: [PreimageUpdate] = []

guard preimages.isSortedAndUnique() else {
throw PreimagesError.preimagesNotSorted
}

for preimage in preimages {
let hash = preimage.data.blake2b256hash()

// check prior state
let prevPreimageData = try await Result {
try await get(serviceAccount: preimage.serviceIndex, preimageHash: hash)
}.mapError { _ in PreimagesError.invalidServiceIndex }.get()

guard prevPreimageData == nil else {
throw PreimagesError.duplicatedPreimage

Check warning on line 64 in Blockchain/Sources/Blockchain/RuntimeProtocols/Preimages.swift

View check run for this annotation

Codecov / codecov/patch

Blockchain/Sources/Blockchain/RuntimeProtocols/Preimages.swift#L64

Added line #L64 was not covered by tests
}

updates.append(PreimageUpdate(
serviceIndex: preimage.serviceIndex,
hash: hash,
data: preimage.data,
length: UInt32(preimage.data.count),
timeslot: timeslot
))
}

return PreimagesPostState(updates: updates)
}
}
37 changes: 4 additions & 33 deletions Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,38 +336,9 @@
}

public func updatePreimages(block: BlockRef, state newState: inout State, prevState: StateRef) async throws {
let preimages = block.extrinsic.preimages.preimages

guard preimages.isSortedAndUnique() else {
throw Error.preimagesNotSorted
}

for preimage in preimages {
let hash = preimage.data.blake2b256hash()

// check prior state
let prevPreimageData: Data? = try await prevState.value.get(serviceAccount: preimage.serviceIndex, preimageHash: hash)
let prevInfo = try await prevState.value.get(
serviceAccount: preimage.serviceIndex, preimageHash: hash, length: UInt32(preimage.data.count)
)
guard prevPreimageData == nil, prevInfo == nil else {
throw Error.duplicatedPreimage
}

// disregard no longer useful ones in new state
let preimageData: Data? = try await newState.get(serviceAccount: preimage.serviceIndex, preimageHash: hash)
let info = try await newState.get(
serviceAccount: preimage.serviceIndex, preimageHash: hash, length: UInt32(preimage.data.count)
)
if preimageData != nil || info != nil {
continue
}

// update state
newState[serviceAccount: preimage.serviceIndex, preimageHash: hash] = preimage.data
newState[
serviceAccount: preimage.serviceIndex, preimageHash: hash, length: UInt32(preimage.data.count)
] = .init([newState.timeslot])
}
let res = try await prevState.value.updatePreimages(
config: config, timeslot: newState.timeslot, preimages: block.extrinsic.preimages
)
newState.mergeWith(postState: res)

Check warning on line 342 in Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift

View check run for this annotation

Codecov / codecov/patch

Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift#L339-L342

Added lines #L339 - L342 were not covered by tests
}
}
10 changes: 10 additions & 0 deletions Blockchain/Sources/Blockchain/State/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,16 @@

extension State: ActivityStatistics {}

extension State: Preimages {
public mutating func mergeWith(postState: PreimagesPostState) {
for update in postState.updates {
self[serviceAccount: update.serviceIndex, preimageHash: update.hash] = update.data
self[serviceAccount: update.serviceIndex, preimageHash: update.hash, length: update.length] =
LimitedSizeArray([update.timeslot])
}
}

Check warning on line 481 in Blockchain/Sources/Blockchain/State/State.swift

View check run for this annotation

Codecov / codecov/patch

Blockchain/Sources/Blockchain/State/State.swift#L475-L481

Added lines #L475 - L481 were not covered by tests
}

struct DummyFunction: AccumulateFunction, OnTransferFunction {
func invoke(
config _: ProtocolConfigRef,
Expand Down
11 changes: 9 additions & 2 deletions Blockchain/Sources/Blockchain/Types/HashAndLength.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import Utils

public struct HashAndLength: Sendable, Codable {
public struct HashAndLength: Sendable, Codable, Comparable {
public var hash: Data32
public var length: DataLength

public init(hash: Data32, length: DataLength) {
self.hash = hash
self.length = length
}

public static func < (lhs: HashAndLength, rhs: HashAndLength) -> Bool {
if lhs.hash == rhs.hash {
return lhs.length < rhs.length

Check warning on line 14 in Blockchain/Sources/Blockchain/Types/HashAndLength.swift

View check run for this annotation

Codecov / codecov/patch

Blockchain/Sources/Blockchain/Types/HashAndLength.swift#L14

Added line #L14 was not covered by tests
}
return lhs.hash < rhs.hash
}
}

extension HashAndLength: Hashable {
Expand All @@ -16,6 +23,6 @@
// and we know the output is 32 bytes
// so we can just take the first 4 bytes and should be good enough
// NOTE: we will never use the Hashable protocol for any critical operations
hasher.combine(hash.data[0 ..< 4])
hasher.combine(hash.data[hash.data.startIndex ..< hash.data.startIndex + 4])
}
}
122 changes: 122 additions & 0 deletions JAMTests/Tests/JAMTests/PreimagesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Blockchain
import Codec
import Foundation
import Testing
import Utils

@testable import JAMTests

struct PreimageInfo: Codable, Equatable, Hashable, Comparable {
var hash: Data32
var blob: Data

static func < (lhs: PreimageInfo, rhs: PreimageInfo) -> Bool {
lhs.hash < rhs.hash
}
}

struct HistoryEntry: Codable, Equatable {
var key: HashAndLength
var value: [TimeslotIndex]
}

struct AccountsMapEntry: Codable, Equatable {
var index: ServiceIndex
@CodingAs<SortedSet<PreimageInfo>> var preimages: Set<PreimageInfo>
@CodingAs<SortedKeyValues<HashAndLength, [TimeslotIndex]>> var history: [HashAndLength: [TimeslotIndex]]
}

struct PreimagesState: Equatable, Codable, Preimages {
var accounts: [AccountsMapEntry] = []

func get(serviceAccount index: ServiceIndex, preimageHash hash: Data32) async throws -> Data? {
for account in accounts where account.index == index {
for preimage in account.preimages where preimage.hash == hash {
return preimage.blob
}
}
return nil

Check warning on line 38 in JAMTests/Tests/JAMTests/PreimagesTests.swift

View check run for this annotation

Codecov / codecov/patch

JAMTests/Tests/JAMTests/PreimagesTests.swift#L37-L38

Added lines #L37 - L38 were not covered by tests
}

func get(serviceAccount index: ServiceIndex, preimageHash hash: Data32,
length: UInt32) async throws -> LimitedSizeArray<TimeslotIndex, ConstInt0, ConstInt3>?
{
for account in accounts where account.index == index {
for history in account.history where history.key.hash == hash && history.key.length == length {
return .init(history.value)
}
}
return nil
}

Check warning on line 50 in JAMTests/Tests/JAMTests/PreimagesTests.swift

View check run for this annotation

Codecov / codecov/patch

JAMTests/Tests/JAMTests/PreimagesTests.swift#L43-L50

Added lines #L43 - L50 were not covered by tests

mutating func mergeWith(postState: PreimagesPostState) {
for update in postState.updates {
let accountIndex = accounts.firstIndex { account in
account.index == update.serviceIndex
}
if let accountIndex {
var account = accounts[accountIndex]
account.preimages.insert(PreimageInfo(hash: update.hash, blob: update.data))
account.history[HashAndLength(hash: update.hash, length: update.length)] = [update.timeslot]
accounts[accountIndex] = account
}
}
}
}

struct PreimagesInput: Codable {
var preimages: ExtrinsicPreimages
var slot: TimeslotIndex
}

struct PreimagesTestcase: Codable {
var input: PreimagesInput
var preState: PreimagesState
var output: UInt8?
var postState: PreimagesState
}

struct PreimagesTests {
static func loadTests() throws -> [Testcase] {
try TestLoader.getTestcases(path: "preimages/data", extension: "bin")
}

func preimagesTests(_ testcase: Testcase, variant: TestVariants) async throws {
let config = variant.config
let decoder = JamDecoder(data: testcase.data, config: config)
let testcase = try decoder.decode(PreimagesTestcase.self)

var state = testcase.preState
let result = await Result {
try await state.updatePreimages(
config: config,
timeslot: testcase.input.slot,
preimages: testcase.input.preimages
)
}

switch result {
case let .success(postState):
switch testcase.output {
case .none:
state.mergeWith(postState: postState)
#expect(state == testcase.postState)
case .some:
Issue.record("Expected error, got \(result)")

Check warning on line 105 in JAMTests/Tests/JAMTests/PreimagesTests.swift

View check run for this annotation

Codecov / codecov/patch

JAMTests/Tests/JAMTests/PreimagesTests.swift#L105

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

Check warning on line 110 in JAMTests/Tests/JAMTests/PreimagesTests.swift

View check run for this annotation

Codecov / codecov/patch

JAMTests/Tests/JAMTests/PreimagesTests.swift#L110

Added line #L110 was not covered by tests
case .some:
// ignore error code because it is unspecified
break
}
}
}

@Test(arguments: try PreimagesTests.loadTests())
func tests(_ testcase: Testcase) async throws {
try await preimagesTests(testcase, variant: .full)
}
}
13 changes: 11 additions & 2 deletions RPC/Package.resolved

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