Skip to content

Commit

Permalink
Safrole (#37)
Browse files Browse the repository at this point in the history
* finish safrole

* add more todos

* more check

* fix

* fix for rethrows
  • Loading branch information
xlc authored Jul 17, 2024
1 parent f62805d commit 73a77c2
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 19 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ tab_width=4
end_of_line=lf
charset=utf-8
trim_trailing_whitespace=true
max_line_length=120
max_line_length=140
insert_final_newline=true

[*.{swift}]
[*.swift]
indent_style=space
tab_width=4
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ disabled_rules:
- file_length
- nesting
- opening_brace
- cyclomatic_complexity

excluded:
- "**/.build"
Expand Down
2 changes: 1 addition & 1 deletion Blockchain/Sources/Blockchain/Blockchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class Blockchain {
}

private func addState(_ state: StateRef) {
stateByBlockHash[state.value.lastBlock.header.hash] = state
stateByBlockHash[state.value.lastBlock.header.hash()] = state
stateByTimeslot[state.value.lastBlock.header.timeslotIndex, default: []].append(state)
}
}
Expand Down
97 changes: 84 additions & 13 deletions Blockchain/Sources/Blockchain/Safrole.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import Utils

public enum SafroleError: Error {
case invalidTimeslot
case tooManyExtrinsics
case extrinsicsNotAllowed
case extrinsicsNotSorted
case extrinsicsTooLow
case extrinsicsNotUnique
case hashingError
case decodingError
case unspecified
Expand Down Expand Up @@ -187,7 +192,7 @@ func pickFallbackValidators(
}

extension Safrole {
public func updateSafrole(slot: TimeslotIndex, entropy: Data32, extrinsics _: ExtrinsicTickets)
public func updateSafrole(slot: TimeslotIndex, entropy: Data32, extrinsics: ExtrinsicTickets)
-> Result<
(
state: SafrolePostState,
Expand All @@ -197,20 +202,35 @@ extension Safrole {
SafroleError
>
{
// E
let epochLength = UInt32(config.value.epochLength)
// Y
let ticketSubmissionEndSlot = UInt32(config.value.ticketSubmissionEndSlot)
// e
let currentEpoch = timeslot / epochLength
// m
let currentPhase = timeslot % epochLength
// e'
let newEpoch = slot / epochLength
// m'
let newPhase = slot % epochLength
let isEpochChange = currentEpoch != newEpoch

guard slot > timeslot else {
return .failure(.invalidTimeslot)
}

do {
let epochLength = UInt32(config.value.epochLength)
let currentEpoch = timeslot / epochLength
// let currentPhase = timeslot % epochLength
let newEpoch = slot / epochLength
let newPhase = slot % epochLength
let isEpochChange = currentEpoch != newEpoch

let isClosingPeriod = newPhase >= UInt32(config.value.ticketSubmissionEndSlot)
if newPhase < ticketSubmissionEndSlot {
guard extrinsics.tickets.count <= config.value.maxTicketsPerExtrinsic else {
return .failure(.tooManyExtrinsics)
}
} else {
guard extrinsics.tickets.isEmpty else {
return .failure(.extrinsicsNotAllowed)
}
}

do {
let (newNextValidators, newCurrentValidators, newPreviousValidators, newTicketsVerifier) = isEpochChange
? (
validatorQueue, // TODO: Φ filter out the one in the punishment set
Expand All @@ -235,7 +255,7 @@ extension Safrole {
BandersnatchPublicKey,
ProtocolConfig.EpochLength
>
> = if newEpoch == currentEpoch + 1, isClosingPeriod, ticketsAccumulator.count == config.value.epochLength {
> = if newEpoch == currentEpoch + 1, newPhase >= ticketSubmissionEndSlot, ticketsAccumulator.count == config.value.epochLength {
.left(ConfigFixedSizeArray(config: config, array: outsideInReorder(ticketsAccumulator.array)))
} else if newEpoch == currentEpoch {
ticketsOrKeys
Expand All @@ -246,18 +266,69 @@ extension Safrole {
))
}

let epochMark = isEpochChange ? EpochMarker(
entropy: entropy,
validators: ConfigFixedSizeArray(config: config, array: newCurrentValidators.map(\.bandersnatch))
) : nil

let ticketsMark: ConfigFixedSizeArray<Ticket, ProtocolConfig.EpochLength>? =
if currentEpoch == newEpoch,
currentPhase < ticketSubmissionEndSlot,
ticketSubmissionEndSlot <= newPhase,
ticketsAccumulator.count == config.value.epochLength {
ConfigFixedSizeArray(
config: config, array: outsideInReorder(ticketsAccumulator.array)
)
} else {
nil
}

let newTickets = extrinsics.getTickets()
guard newTickets.isSorted() else {
return .failure(.extrinsicsNotSorted)
}

var newTicketsAccumulatorArr = if isEpochChange {
[Ticket]()
} else {
ticketsAccumulator.array
}

try newTicketsAccumulatorArr.insertSorted(newTickets) {
if $0 == $1 {
throw SafroleError.extrinsicsNotUnique
}
return $0 < $1
}

if newTicketsAccumulatorArr.count > config.value.epochLength {
let firstToBeRemoved = newTicketsAccumulatorArr[config.value.epochLength]
let highestTicket = newTickets.last! // newTickets must not be empty, otherwise we won't need to remove anything
guard highestTicket < firstToBeRemoved else {
// every tickets must be valid or this is an invalid block
// i.e. the block producer must not include invalid tickets
throw SafroleError.extrinsicsTooLow
}
newTicketsAccumulatorArr.removeLast(newTicketsAccumulatorArr.count - config.value.epochLength)
}

let newTicketsAccumulator = ConfigLimitedSizeArray<Ticket, ProtocolConfig.Int0, ProtocolConfig.EpochLength>(
config: config,
array: newTicketsAccumulatorArr
)

let postState = SafrolePostState(
timeslot: slot,
entropyPool: newEntropyPool,
previousValidators: newPreviousValidators,
currentValidators: newCurrentValidators,
nextValidators: newNextValidators,
validatorQueue: validatorQueue,
ticketsAccumulator: ticketsAccumulator,
ticketsAccumulator: newTicketsAccumulator,
ticketsOrKeys: newTicketsOrKeys,
ticketsVerifier: newTicketsVerifier
)
return .success((postState, nil, nil))
return .success((state: postState, epochMark: epochMark, ticketsMark: ticketsMark))
} catch let e as SafroleError {
return .failure(e)
} catch Blake2Error.hashingError {
Expand Down
11 changes: 11 additions & 0 deletions Blockchain/Sources/Blockchain/Types/ExtrinsicTickets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,14 @@ extension ExtrinsicTickets: ScaleCodec.Encodable {
try encoder.encode(tickets)
}
}

extension ExtrinsicTickets {
public func getTickets() -> [Ticket] {
tickets.array.map {
// TODO: fix this
// this should be the Bandersnatch VRF output
let ticketId = Data32($0.signature.data[0 ..< 32])!
return Ticket(id: ticketId, attempt: $0.attempt)
}
}
}
8 changes: 6 additions & 2 deletions Blockchain/Sources/Blockchain/Types/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ extension Header: ScaleCodec.Encodable {
}

extension Header {
public var hash: Data32 {
Data32() // TODO: implement this
public func hash() -> Data32 {
do {
return try blake2b256(ScaleCodec.encode(self))
} catch let e {
fatalError("Failed to hash header: \(e)")
}
}
}
6 changes: 6 additions & 0 deletions Blockchain/Sources/Blockchain/Types/Ticket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ extension Ticket: ScaleCodec.Codable {
try encoder.encode(attempt)
}
}

extension Ticket: Comparable {
public static func < (lhs: Ticket, rhs: Ticket) -> Bool {
(lhs.id, lhs.attempt) < (rhs.id, rhs.attempt)
}
}
37 changes: 37 additions & 0 deletions Utils/Sources/Utils/Array+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
extension Array where Element: Comparable {
public func isSorted(by comparer: (Element, Element) -> Bool = { $0 < $1 }) -> Bool {
var previous: Element?
for element in self {
if let previous {
if !comparer(previous, element), previous != element {
return false
}
}
previous = element
}
return true
}

/// Insert the elements of the given sequence to the array, in sorted order.
///
/// - Parameter elements: The elements to insert.
/// - Parameter comparer: The comparison function to use to determine the order of the elements.
/// - Complexity: O(*n*), where *n* is the number of elements in the sequence.
///
/// - Note: The elements of the sequence must be comparable.
/// - Invariant: The array and elements must be sorted according to the given comparison function.
public mutating func insertSorted(_ elements: any Sequence<Element>, by comparer: ((Element, Element) throws -> Bool)? = nil) rethrows {
reserveCapacity(count + elements.underestimatedCount)
let comparer = comparer ?? { $0 < $1 }
var startIdx = 0
for element in elements {
if let idx = try self[startIdx...].firstIndex(where: { try !comparer($0, element) }) {
insert(element, at: idx)
startIdx = idx + 1
} else {
append(element)
startIdx = endIndex
}
}
}
}
File renamed without changes.
12 changes: 12 additions & 0 deletions Utils/Sources/Utils/FixedSizeData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ extension FixedSizeData: CustomStringConvertible, CustomDebugStringConvertible {
}
}

extension FixedSizeData: Comparable {
public static func < (lhs: Self, rhs: Self) -> Bool {
guard lhs.data.count == rhs.data.count else {
return lhs.data.count < rhs.data.count
}
for (l, r) in zip(lhs.data, rhs.data) where l != r {
return l < r
}
return false
}
}

extension FixedSizeData: ScaleCodec.Codable {
public init(from decoder: inout some ScaleCodec.Decoder) throws {
try self.init(decoder.decode(Data.self, .fixed(UInt(T.value))))!
Expand Down
56 changes: 56 additions & 0 deletions Utils/Tests/UtilsTests/ArrayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import Testing

@testable import Utils

struct ArrayTests {
@Test(arguments: [
[], [1], [2, 4], [5, 19, 34, 34, 56, 56],
])
func isSorted(testCase: [Int]) {
#expect(testCase.isSorted())
}

@Test(arguments: [
[], [1], [4, 2], [56, 56, 34, 34, 19, 5]
])
func isSortedBy(testCase: [Int]) {
#expect(testCase.isSorted(by: >))
}

@Test(arguments: [
[4, 2], [56, 56, 34, 35, 19, 5], [1, 3, 2]
])
func notSorted(testCase: [Int]) {
#expect(!testCase.isSorted())
}

@Test(arguments: [
([], []),
([], [1]),
([1], []),
([1, 2, 3], [1, 2, 3]),
([1, 10, 20, 30], [2, 12, 22, 32, 42]),
([1, 2, 3], [4, 5, 6]),
([4, 5, 6], [1, 2, 3]),
([1, 5, 10, 30], [6, 7, 8, 9, 10, 11]),
])
func insertSorted(testCase: ([Int], [Int])) {
var arr = testCase.0
arr.insertSorted(testCase.1)
#expect(arr.isSorted())
#expect(arr == (testCase.0 + testCase.1).sorted())
}

@Test(arguments: [
([3, 3, 2, 2, 1, 1], [3, 2, 1]),
([6, 5, 4], [4, 3, 2]),
([10, 5, 3, 2], [11, 10, 4, 3, 3, 2, 1]),
])
func insertSortedBy(testCase: ([Int], [Int])) {
var arr = testCase.0
arr.insertSorted(testCase.1, by: >)
#expect(arr.isSorted(by: >))
#expect(arr == (testCase.0 + testCase.1).sorted(by: >))
}
}
22 changes: 21 additions & 1 deletion Utils/Tests/UtilsTests/Data32Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Testing

@testable import Utils

@Suite struct Data32Tests {
struct Data32Tests {
@Test func testZero() throws {
let value = Data32()
#expect(value.data == Data(repeating: 0, count: 32))
Expand All @@ -30,4 +30,24 @@ import Testing
#expect(Data32(Data(repeating: 0, count: 31)) == nil)
#expect(Data32(Data(repeating: 0, count: 33)) == nil)
}

@Test func testComparable() throws {
let data1 = Data(repeating: 0, count: 32)
var data2 = data1
data2[31] = 1
var data3 = data1
data3[1] = 1

let a = Data32(data1)!
let b = Data32(data2)!
let c = Data32(data3)!

#expect(a < b)
#expect(b < c)
#expect(a < c)

var arr = [c, b, a]
arr.sort()
#expect(arr == [a, b, c])
}
}

0 comments on commit 73a77c2

Please sign in to comment.