From 317e274a8a1e96b37fa2276f150550e5ae4b8e90 Mon Sep 17 00:00:00 2001 From: MacOMNI <414294494@qq.com> Date: Fri, 23 Aug 2024 06:35:46 +0800 Subject: [PATCH] update bitstring & test (#67) * change preimage size to data count * update bitstring test * update Bitstring & test * fix some issues about bitstring & test * update FixSizeBitstring * Update Utils/Sources/Utils/FixSizeBitstring.swift Co-authored-by: Xiliang Chen * Update Utils/Sources/Utils/FixSizeBitstring.swift Co-authored-by: Xiliang Chen * Update Utils/Sources/Utils/FixSizeBitstring.swift Co-authored-by: Xiliang Chen * Update Utils/Sources/Utils/FixSizeBitstring.swift Co-authored-by: Xiliang Chen * Update Utils/Sources/Utils/FixSizeBitstring.swift Co-authored-by: Xiliang Chen * update bitstring * Update .vscode/launch.json * Implements JamCodec and replace with ScaleCodec (#68) * use Swift Codable * Codec package * implement codec * more refactor * fmt * more fix * disable safrole tests * comment out safrole tests * #update FixSizeBitstring * update config init * change assurance type to bitstring & test * refactor --------- Co-authored-by: Xiliang Chen --- .../Types/ExtrinsicAvailability.swift | 4 +- Codec/Sources/Codec/FixedLengthCodable.swift | 2 +- Codec/Sources/Codec/JamDecoder.swift | 2 +- Codec/Sources/Codec/JamEncoder.swift | 2 +- Utils/Sources/Utils/ConfigSizeBitString.swift | 134 ++++++++++++++++++ .../UtilsTests/ConfigSizeBitStringTests.swift | 116 +++++++++++++++ 6 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 Utils/Sources/Utils/ConfigSizeBitString.swift create mode 100644 Utils/Tests/UtilsTests/ConfigSizeBitStringTests.swift diff --git a/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift b/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift index bce621b1..c3a2d9ba 100644 --- a/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift +++ b/Blockchain/Sources/Blockchain/Types/ExtrinsicAvailability.swift @@ -6,7 +6,7 @@ public struct ExtrinsicAvailability: Sendable, Equatable, Codable { // a public var parentHash: Data32 // f - public var assurance: Data // bit string with length of Constants.TotalNumberOfCores TODO: use a BitString type + public var assurance: ConfigSizeBitString // v public var validatorIndex: ValidatorIndex // s @@ -14,7 +14,7 @@ public struct ExtrinsicAvailability: Sendable, Equatable, Codable { public init( parentHash: Data32, - assurance: Data, + assurance: ConfigSizeBitString, validatorIndex: ValidatorIndex, signature: Ed25519Signature ) { diff --git a/Codec/Sources/Codec/FixedLengthCodable.swift b/Codec/Sources/Codec/FixedLengthCodable.swift index 35879ec1..0df86aa0 100644 --- a/Codec/Sources/Codec/FixedLengthCodable.swift +++ b/Codec/Sources/Codec/FixedLengthCodable.swift @@ -3,6 +3,6 @@ import Foundation public protocol FixedLengthData { var data: Data { get } - static func length(decoder: Decoder) -> Int + static func length(decoder: Decoder) throws -> Int init(decoder: Decoder, data: Data) throws } diff --git a/Codec/Sources/Codec/JamDecoder.swift b/Codec/Sources/Codec/JamDecoder.swift index 284a3fd9..b5c05352 100644 --- a/Codec/Sources/Codec/JamDecoder.swift +++ b/Codec/Sources/Codec/JamDecoder.swift @@ -160,7 +160,7 @@ private class DecodeContext: Decoder { fileprivate func decodeFixedLengthData(_ type: T.Type, key: CodingKey?) throws -> T { try withExtendedLifetime(PushCodingPath(decoder: self, key: key)) { - let length = type.length(decoder: self) + let length = try type.length(decoder: self) guard data.count >= length else { throw DecodingError.dataCorrupted( DecodingError.Context( diff --git a/Codec/Sources/Codec/JamEncoder.swift b/Codec/Sources/Codec/JamEncoder.swift index e7532cee..ef5b07e0 100644 --- a/Codec/Sources/Codec/JamEncoder.swift +++ b/Codec/Sources/Codec/JamEncoder.swift @@ -40,8 +40,8 @@ private class EncodeContext: Encoder { // reserve capacity for the length // length is variable size but very unlikely to be larger than 4 bytes data.reserveCapacity(data.count + value.count + (lengthPrefix ? 4 : 0)) - let length = UInt32(value.count) if lengthPrefix { + let length = UInt32(value.count) data.append(contentsOf: length.encode(method: .variableWidth)) } data.append(value) diff --git a/Utils/Sources/Utils/ConfigSizeBitString.swift b/Utils/Sources/Utils/ConfigSizeBitString.swift new file mode 100644 index 00000000..6f110aef --- /dev/null +++ b/Utils/Sources/Utils/ConfigSizeBitString.swift @@ -0,0 +1,134 @@ +import Codec +import Foundation + +public enum ConfigSizeBitStringError: Error { + case missingConfig + case invalidData + case invalidIndex +} + +public struct ConfigSizeBitString: Equatable, Sendable, Codable { + /// Byte storage for bits. + private var bytes: Data + /// Bit length + public let length: Int + + private var byteLength: Int { + (length + 7) / 8 + } + + public init(config: TBitLength.TConfig, data: Data) throws(ConfigSizeBitStringError) { + length = TBitLength.read(config: config) + bytes = data + + if byteLength != data.count { + throw .invalidData + } + } + + public init(config: TBitLength.TConfig) { + length = TBitLength.read(config: config) + let byteLength = (length + 7) / 8 + bytes = Data(repeating: 0, count: byteLength) + } + + private func at(unchecked index: Int) -> Bool { + let byteIndex = index / 8 + let bitIndex = index % 8 + return (bytes[byteIndex] & (1 << bitIndex)) != 0 + } + + /// Formats the bitstring in binary digits. + public var binaryString: String { + (0 ..< length).map { at(unchecked: $0) ? "1" : "0" }.joined() + } + + public var description: String { binaryString } + + public func at(_ index: Int) throws(ConfigSizeBitStringError) -> Bool { + guard index < length else { + throw .invalidIndex + } + return at(unchecked: index) + } + + public mutating func set(_ index: Int, to value: Bool) throws(ConfigSizeBitStringError) { + guard index < length else { + throw .invalidIndex + } + let byteIndex = index / 8 + let bitIndex = index % 8 + if value { + bytes[byteIndex] |= (1 << bitIndex) + } else { + bytes[byteIndex] &= ~(1 << bitIndex) + } + } +} + +extension ConfigSizeBitString: RandomAccessCollection { + public typealias Element = Bool + + public var startIndex: Int { + 0 + } + + public var endIndex: Int { + length + } + + public subscript(position: Int) -> Bool { + get { + try! at(position) + } + set { + try! set(position, to: newValue) + } + } + + public func index(after i: Int) -> Int { + i + 1 + } + + public func index(before i: Int) -> Int { + i - 1 + } + + public func index(_ i: Int, offsetBy distance: Int) -> Int { + i + distance + } + + public func index(_ i: Int, offsetBy distance: Int, limitedBy limit: Int) -> Int? { + i + distance < limit ? i + distance : nil + } + + public func distance(from start: Int, to end: Int) -> Int { + end - start + } + + public func formIndex(after i: inout Int) { + i += 1 + } + + public func formIndex(before i: inout Int) { + i -= 1 + } +} + +extension ConfigSizeBitString: FixedLengthData { + public var data: Data { bytes } + + public static func length(decoder: Decoder) throws -> Int { + guard let config = decoder.getConfig(TBitLength.TConfig.self) else { + throw ConfigSizeBitStringError.missingConfig + } + return (TBitLength.read(config: config) + 7) / 8 + } + + public init(decoder: Decoder, data: Data) throws { + guard let config = decoder.getConfig(TBitLength.TConfig.self) else { + throw ConfigSizeBitStringError.missingConfig + } + try self.init(config: config, data: data) + } +} diff --git a/Utils/Tests/UtilsTests/ConfigSizeBitStringTests.swift b/Utils/Tests/UtilsTests/ConfigSizeBitStringTests.swift new file mode 100644 index 00000000..074c57de --- /dev/null +++ b/Utils/Tests/UtilsTests/ConfigSizeBitStringTests.swift @@ -0,0 +1,116 @@ +import Codec +import Foundation +import Testing + +@testable import Utils + +struct ReadIntValue: ReadInt { + typealias TConfig = Int + + static func read(config: Int) -> Int { + config + } +} + +struct ConfigSizeBitStringTests { + @Test func notEnoughData() throws { + let data = Data([0]) + let length = 9 + #expect(throws: ConfigSizeBitStringError.invalidData) { + try ConfigSizeBitString(config: length, data: data) + } + } + + @Test func tooMuchData() throws { + let data = Data([0, 0]) + let length = 8 + #expect(throws: ConfigSizeBitStringError.invalidData) { + try ConfigSizeBitString(config: length, data: data) + } + } + + @Test func works() throws { + var value = ConfigSizeBitString(config: 7) + #expect(value.binaryString == "0000000") + + value[0] = true + #expect(Array(value) == [true, false, false, false, false, false, false]) + #expect(value.binaryString == "1000000") + + value[1] = true + #expect(Array(value) == [true, true, false, false, false, false, false]) + #expect(value.binaryString == "1100000") + + value[6] = true + #expect(Array(value) == [true, true, false, false, false, false, true]) + #expect(value.binaryString == "1100001") + + value[0] = false + #expect(Array(value) == [false, true, false, false, false, false, true]) + #expect(value.binaryString == "0100001") + } + + @Test func initWorks() throws { + let data = Data([0b1011_0101]) + let length = 7 + let value = try ConfigSizeBitString(config: length, data: data) + #expect(Array(value) == [true, false, true, false, true, true, false]) + #expect(value.binaryString == "1010110") + } + + @Test func largeInitWorks() throws { + let data = Data([0b1011_0101, 0b1100_0101, 0b0010_0110]) + let length = 20 + var value = try ConfigSizeBitString(config: length, data: data) + #expect(Array(value) == [ + true, false, true, false, + true, true, false, true, + true, false, true, false, + false, false, true, true, + false, true, true, false, + ]) + #expect(value.binaryString == "10101101101000110110") + + value[19] = true + #expect(value[19] == true) + #expect(value.binaryString == "10101101101000110111") + + #expect(throws: ConfigSizeBitStringError.invalidIndex) { + _ = try value.at(20) + } + + #expect(throws: ConfigSizeBitStringError.invalidIndex) { + try value.set(20, to: true) + } + } + + @Test func codable() throws { + let data = Data([0b1011_0101, 0b1100_0101, 0b0000_0110]) + let length = 20 + let value = try ConfigSizeBitString(config: length, data: data) + + let encoded = try JamEncoder.encode(value) + #expect(encoded == data) + let decoded = try JamDecoder.decode(ConfigSizeBitString.self, from: encoded, withConfig: length) + #expect(decoded == value) + } + + @Test func equatable() throws { + let data = Data([0b1011_0101, 0b1100_0101, 0b0000_0110]) + let length = 20 + var value = try ConfigSizeBitString(config: length, data: data) + + let length2 = 21 + let value2 = try ConfigSizeBitString(config: length2, data: data) + + #expect(value == value) + #expect(value != value2) + + var value3 = value + value3[19] = true + #expect(value3 != value) + + value[19] = true + #expect(value == value3) + } +}