diff --git a/Blockchain/Sources/Blockchain/IsAuthorizedFunction.swift b/Blockchain/Sources/Blockchain/IsAuthorizedFunction.swift new file mode 100644 index 00000000..5e8df8e0 --- /dev/null +++ b/Blockchain/Sources/Blockchain/IsAuthorizedFunction.swift @@ -0,0 +1,38 @@ +import Codec +import Foundation +import PolkaVM + +public protocol IsAuthorizedFunction { + func invoke( + config: ProtocolConfigRef, + package: WorkPackage, + coreIndex: CoreIndex + ) throws -> Result +} + +extension IsAuthorizedFunction { + public func invoke(config: ProtocolConfigRef, package: WorkPackage, coreIndex: CoreIndex) throws -> Result { + var ctx = IsAuthorizedContext() + let args = try JamEncoder.encode(package) + JamEncoder.encode(coreIndex) + let (exitReason, _, _, output) = invokePVM( + config: config, + blob: package.authorizationCodeHash.data, + pc: 0, + gas: config.value.workPackageAuthorizerGas, + argumentData: args, + ctx: &ctx + ) + switch exitReason { + case .outOfGas: + return .failure(.outOfGas) + case .panic(.trap): + return .failure(.panic) + default: + if let output { + return .success(output) + } else { + return .failure(.panic) + } + } + } +} diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift index 77e8caf8..3dc1d9f7 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Disputes.swift @@ -10,6 +10,7 @@ public enum DisputeError: Error { case invalidJudgementsCount case expectInFaults case expectInCulprits + case invalidPublicKey } public struct ReportItem: Sendable, Equatable, Codable { @@ -99,7 +100,10 @@ extension Disputes { let prefix = judgement.isValid ? SigningContext.valid : SigningContext.invalid let payload = prefix + verdict.reportHash.data - guard Ed25519.verify(signature: judgement.signature, message: payload, publicKey: signer) else { + let pubkey = try Result { try Ed25519.PublicKey(from: signer) } + .mapError { _ in DisputeError.invalidPublicKey } + .get() + guard pubkey.verify(signature: judgement.signature, message: payload) else { throw .invalidJudgementSignature } } diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift index 3c9058c6..92f5d055 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Guaranteeing.swift @@ -12,6 +12,7 @@ public enum GuaranteeingError: Error { case duplicatedWorkPackage case prerequistieNotFound case invalidResultCodeHash + case invalidPublicKey } public protocol Guaranteeing { @@ -104,7 +105,10 @@ extension Guaranteeing { let reportHash = report.hash() workReportHashes.insert(reportHash) let payload = SigningContext.guarantee + reportHash.data - guard Ed25519.verify(signature: credential.signature, message: payload, publicKey: key) else { + let pubkey = try Result { try Ed25519.PublicKey(from: key) } + .mapError { _ in GuaranteeingError.invalidPublicKey } + .get() + guard pubkey.verify(signature: credential.signature, message: payload) else { throw .invalidGuaranteeSignature } diff --git a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift index 044712a7..45fc6c9e 100644 --- a/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift +++ b/Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift @@ -216,7 +216,8 @@ public final class Runtime { let hash = Blake2b256.hash(assurance.parentHash, assurance.assurance) let payload = SigningContext.available + hash.data let validatorKey = try newState.currentValidators.at(Int(assurance.validatorIndex)) - guard Ed25519.verify(signature: assurance.signature, message: payload, publicKey: validatorKey.ed25519) else { + let pubkey = try Ed25519.PublicKey(from: validatorKey.ed25519) + guard pubkey.verify(signature: assurance.signature, message: payload) else { throw Error.invalidAssuranceSignature } } diff --git a/Blockchain/Sources/Blockchain/Types/ExtrinsicDisputes.swift b/Blockchain/Sources/Blockchain/Types/ExtrinsicDisputes.swift index 42e6702f..f9ff8912 100644 --- a/Blockchain/Sources/Blockchain/Types/ExtrinsicDisputes.swift +++ b/Blockchain/Sources/Blockchain/Types/ExtrinsicDisputes.swift @@ -107,6 +107,7 @@ extension ExtrinsicDisputes: Validate { case judgementsNotSorted case invalidCulpritSignature case invalidFaultSignature + case invalidPublicKey } public func validate(config _: Config) throws(Error) { @@ -130,7 +131,10 @@ extension ExtrinsicDisputes: Validate { for culprit in culprits { let payload = SigningContext.guarantee + culprit.reportHash.data - guard Ed25519.verify(signature: culprit.signature, message: payload, publicKey: culprit.validatorKey) else { + let pubkey = try Result { try Ed25519.PublicKey(from: culprit.validatorKey) } + .mapError { _ in Error.invalidPublicKey } + .get() + guard pubkey.verify(signature: culprit.signature, message: payload) else { throw .invalidCulpritSignature } } @@ -138,7 +142,10 @@ extension ExtrinsicDisputes: Validate { for fault in faults { let prefix = fault.vote ? SigningContext.valid : SigningContext.invalid let payload = prefix + fault.reportHash.data - guard Ed25519.verify(signature: fault.signature, message: payload, publicKey: fault.validatorKey) else { + let pubkey = try Result { try Ed25519.PublicKey(from: fault.validatorKey) } + .mapError { _ in Error.invalidPublicKey } + .get() + guard pubkey.verify(signature: fault.signature, message: payload) else { throw .invalidFaultSignature } } diff --git a/Blockchain/Sources/Blockchain/Validator/KeyStore.swift b/Blockchain/Sources/Blockchain/Validator/KeyStore.swift deleted file mode 100644 index 597cddb5..00000000 --- a/Blockchain/Sources/Blockchain/Validator/KeyStore.swift +++ /dev/null @@ -1 +0,0 @@ -public protocol KeyStore {} diff --git a/Blockchain/Sources/Blockchain/Validator/Validator.swift b/Blockchain/Sources/Blockchain/Validator/Validator.swift index f7b4ebd2..a550507c 100644 --- a/Blockchain/Sources/Blockchain/Validator/Validator.swift +++ b/Blockchain/Sources/Blockchain/Validator/Validator.swift @@ -1,3 +1,5 @@ +import Utils + public class Validator { private let blockchain: Blockchain private var keystore: KeyStore diff --git a/Blockchain/Tests/BlockchainTests/ValidateTests.swift b/Blockchain/Tests/BlockchainTests/ValidateTests.swift index ead03a67..2a33d551 100644 --- a/Blockchain/Tests/BlockchainTests/ValidateTests.swift +++ b/Blockchain/Tests/BlockchainTests/ValidateTests.swift @@ -2,7 +2,7 @@ import Testing // @retroactive to slient Equtable warning -extension ValidateError: @retroactive Equatable { +extension ValidateError: Swift.Equatable { public static func == (lhs: ValidateError, rhs: ValidateError) -> Bool { String(describing: lhs) == String(describing: rhs) } diff --git a/PolkaVM/Sources/PolkaVM/Engine.swift b/PolkaVM/Sources/PolkaVM/Engine.swift index ae2f6062..d4f5d1d6 100644 --- a/PolkaVM/Sources/PolkaVM/Engine.swift +++ b/PolkaVM/Sources/PolkaVM/Engine.swift @@ -5,9 +5,11 @@ private let logger = Logger(label: "Engine") public class Engine { let config: PvmConfig + let hostCallContext: (any HostCallContext)? - public init(config: PvmConfig) { + public init(config: PvmConfig, hostCallContext: (any HostCallContext)? = nil) { self.config = config + self.hostCallContext = hostCallContext } public func execute(program: ProgramCode, state: VMState) -> ExitReason { @@ -17,12 +19,43 @@ public class Engine { return .outOfGas } if case let .exit(reason) = step(program: program, context: context) { - return reason + switch reason { + case let .hostCall(callIndex): + if case let .exit(hostExitReason) = hostCall(state: state, callIndex: callIndex) { + return hostExitReason + } + default: + return reason + } } } } - public func step(program: ProgramCode, context: ExecutionContext) -> ExecOutcome { + func hostCall(state: VMState, callIndex: UInt32) -> ExecOutcome { + guard let hostCallContext else { + return .exit(.panic(.trap)) + } + + let result = hostCallContext.dispatch(index: callIndex, state: state) + switch result { + case let .exit(reason): + switch reason { + case let .pageFault(address): + return .exit(.pageFault(address)) + case let .hostCall(callIndexInner): + let pc = state.pc + let skip = state.program.skip(pc) + state.increasePC(skip + 1) + return hostCall(state: state, callIndex: callIndexInner) + default: + return .exit(reason) + } + case .continued: + return .continued + } + } + + func step(program: ProgramCode, context: ExecutionContext) -> ExecOutcome { let pc = context.state.pc let skip = program.skip(pc) let startIndex = program.code.startIndex + Int(pc) diff --git a/PolkaVM/Sources/PolkaVM/HostCall/Context.swift b/PolkaVM/Sources/PolkaVM/HostCall/Context.swift new file mode 100644 index 00000000..b8d357a4 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/HostCall/Context.swift @@ -0,0 +1,8 @@ +public protocol HostCallContext { + associatedtype ContextType + + var context: ContextType { get set } + + /// host-call dispatch function + func dispatch(index: UInt32, state: VMState) -> ExecOutcome +} diff --git a/PolkaVM/Sources/PolkaVM/HostCall/Function.swift b/PolkaVM/Sources/PolkaVM/HostCall/Function.swift new file mode 100644 index 00000000..91eec3d1 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/HostCall/Function.swift @@ -0,0 +1,9 @@ +public protocol HostCallFunction { + static var identifier: UInt8 { get } + static var gasCost: UInt8 { get } + + associatedtype Input + associatedtype Output + + static func call(state: VMState, input: Input) throws -> Output +} diff --git a/PolkaVM/Sources/PolkaVM/HostCall/Functions.swift b/PolkaVM/Sources/PolkaVM/HostCall/Functions.swift new file mode 100644 index 00000000..d84eabbc --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/HostCall/Functions.swift @@ -0,0 +1,13 @@ +public class Gas: HostCallFunction { + public static var identifier: UInt8 { 0 } + public static var gasCost: UInt8 { 10 } + + public typealias Input = Void + + public typealias Output = Void + + public static func call(state: VMState, input _: Input) -> Output { + state.writeRegister(Registers.Index(raw: 0), UInt32(bitPattern: Int32(state.getGas() & 0xFFFF_FFFF))) + state.writeRegister(Registers.Index(raw: 1), UInt32(bitPattern: Int32(state.getGas() >> 32))) + } +} diff --git a/PolkaVM/Sources/PolkaVM/HostCall/ResultConstants.swift b/PolkaVM/Sources/PolkaVM/HostCall/ResultConstants.swift new file mode 100644 index 00000000..99c2ad16 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/HostCall/ResultConstants.swift @@ -0,0 +1,36 @@ +public enum HostCallResultCode: UInt32 { + /// NONE = 2^32 − 1: The return value indicating an item does not exist. + case NONE = 0xFFFF_FFFF + /// WHAT = 2^32 − 2: Name unknown. + case WHAT = 0xFFFF_FFFE + /// OOB = 2^32 − 3: The return value for when a memory index is provided for reading/writing which is not accessible. + case OOB = 0xFFFF_FFFD + /// WHO = 2^32 − 4: Index unknown. + case WHO = 0xFFFF_FFFC + /// FULL = 2^32 − 5: Storage full. + case FULL = 0xFFFF_FFFB + /// CORE = 2^32 − 6: Core index unknown. + case CORE = 0xFFFF_FFFA + /// CASH = 2^32 − 7: Insufficient funds. + case CASH = 0xFFFF_FFF9 + /// LOW = 2^32 − 8: Gas limit too low. + case LOW = 0xFFFF_FFF8 + /// HIGH = 2^32 − 9: Gas limit too high. + case HIGH = 0xFFFF_FFF7 + /// HUH = 2^32 − 10: The item is already solicited or cannot be forgotten. + case HUH = 0xFFFF_FFF6 + /// OK = 0: The return value indicating general success. + case OK = 0 +} + +// Inner pvm invocations have their own set of result codes👇 +public enum HostCallResultCodeInner: UInt32 { + /// HALT = 0: The invocation completed and halted normally. + case HALT = 0 + /// PANIC = 1: The invocation completed with a panic. + case PANIC = 1 + /// FAULT = 2: The invocation completed with a page fault. + case FAULT = 2 + /// HOST = 3: The invocation completed with a host-call fault. + case HOST = 3 +} diff --git a/PolkaVM/Sources/PolkaVM/InvocationContexts/IsAuthorizedContext.swift b/PolkaVM/Sources/PolkaVM/InvocationContexts/IsAuthorizedContext.swift new file mode 100644 index 00000000..fab747cc --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/InvocationContexts/IsAuthorizedContext.swift @@ -0,0 +1,19 @@ +import Foundation + +public class IsAuthorizedContext: HostCallContext { + public typealias ContextType = Void + + public var context: ContextType = () + + public init() {} + + public func dispatch(index: UInt32, state: VMState) -> ExecOutcome { + if index == Gas.identifier { + Gas.call(state: state, input: ()) + } else { + state.consumeGas(10) + state.writeRegister(Registers.Index(raw: 0), HostCallResultCode.WHAT.rawValue) + } + return .continued + } +} diff --git a/PolkaVM/Sources/PolkaVM/Memory.swift b/PolkaVM/Sources/PolkaVM/Memory.swift index 38c9ac2d..c4bdaa71 100644 --- a/PolkaVM/Sources/PolkaVM/Memory.swift +++ b/PolkaVM/Sources/PolkaVM/Memory.swift @@ -202,7 +202,7 @@ public class Memory { try getSection(forAddress: address).read(address: address, length: 1).first ?? 0 } - public func read(address: UInt32, length: Int) throws -> Data { + public func read(address: UInt32, length: Int) throws(Error) -> Data { try getSection(forAddress: address).read(address: address, length: length) } diff --git a/PolkaVM/Sources/PolkaVM/ProgramCode.swift b/PolkaVM/Sources/PolkaVM/ProgramCode.swift index 5bbd700b..3aea7d5e 100644 --- a/PolkaVM/Sources/PolkaVM/ProgramCode.swift +++ b/PolkaVM/Sources/PolkaVM/ProgramCode.swift @@ -98,7 +98,7 @@ public class ProgramCode { /// traverse the program code and collect basic block indices private static func getBasicBlockIndices(code: Data, bitmask: Data) -> Set { - // TODO: parse the instructions here and so we don't need to do skip calculation twice + // TODO: parse the instructions here and so we don't need to do skip calculation multiple times var res: Set = [0] var i = UInt32(0) while i < code.count { diff --git a/PolkaVM/Sources/PolkaVM/Registers.swift b/PolkaVM/Sources/PolkaVM/Registers.swift index b290a978..0fdccd85 100644 --- a/PolkaVM/Sources/PolkaVM/Registers.swift +++ b/PolkaVM/Sources/PolkaVM/Registers.swift @@ -14,6 +14,10 @@ public struct Registers: Equatable { public init(rd: UInt8) { value = min(rd, 12) } + + public init(raw: UInt8) { + value = raw + } } public var reg1: UInt32 = 0 diff --git a/PolkaVM/Sources/PolkaVM/invokePVM.swift b/PolkaVM/Sources/PolkaVM/invokePVM.swift new file mode 100644 index 00000000..eb730c30 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/invokePVM.swift @@ -0,0 +1,33 @@ +import Foundation +import TracingUtils + +private let logger = Logger(label: "invokePVM") + +/// common PVM program-argument invocation function +public func invokePVM(config: PvmConfig, blob: Data, pc: UInt32, gas: UInt64, argumentData: Data?, + ctx: inout some HostCallContext) -> (ExitReason, VMState?, UInt64?, Data?) +{ + do { + let state = try VMState(standardProgramBlob: blob, pc: pc, gas: gas, argumentData: argumentData) + let engine = Engine(config: config, hostCallContext: ctx) + let exitReason = engine.execute(program: state.program, state: state) + + switch exitReason { + case .outOfGas: + return (.outOfGas, state, nil, nil) + case .halt: + let (reg10, reg11) = state.readRegister(Registers.Index(raw: 10), Registers.Index(raw: 11)) + // TODO: check if this is correct + let output = try? state.readMemory(address: reg10, length: Int(reg11 - reg10)) + return (.halt, state, UInt64(state.getGas()), output ?? Data()) + default: + return (.panic(.trap), state, nil, nil) + } + } catch let e as StandardProgram.Error { + logger.error("standard program initialization failed: \(e)") + return (.panic(.trap), nil, nil, nil) + } catch let e { + logger.error("unknown error: \(e)") + return (.panic(.trap), nil, nil, nil) + } +} diff --git a/Utils/Sources/Utils/Crypto/BLS.swift b/Utils/Sources/Utils/Crypto/BLS.swift index 94ebd206..f3ccf422 100644 --- a/Utils/Sources/Utils/Crypto/BLS.swift +++ b/Utils/Sources/Utils/Crypto/BLS.swift @@ -1,106 +1,135 @@ import blst import Foundation -enum BLSError: Error { - case ikmTooShort - case blstError(BLST_ERROR) -} - /// A wrapper to blst C library. /// /// `blst_p1` for public keys, and `blst_p2` for signatures -public struct BLS { - public let secretKey: Data32 - - public let publicKey: Data48 - - /// Initiate a BLS secret key with IKM. - /// IKM MUST be infeasible to guess, e.g., generated by a trusted source of randomness. - /// IKM MUST be at least 32 bytes long, but it MAY be longer. - public init(ikm: Data) throws { - guard ikm.count >= 32 else { - throw BLSError.ikmTooShort - } +public enum BLS: KeyType { + public enum Error: Swift.Error { + case blstError(BLST_ERROR) + } - var sk = blst_scalar() - let ikmBytes = [UInt8](ikm) - let ikmLen = ikmBytes.count + public final class SecretKey: SecretKeyProtocol { + // this is immutable after initialization but C API requires it to be mutable + private var sk: blst_scalar - blst_keygen(&sk, ikmBytes, ikmLen, nil, 0) + public let publicKey: PublicKey - var out = [UInt8](repeating: 0, count: 32) - blst_bendian_from_scalar(&out, &sk) + /// Initiate a BLS secret key with IKM. + /// IKM MUST be infeasible to guess, e.g., generated by a trusted source of randomness. + /// IKM MUST be at least 32 bytes long, but it MAY be longer. + public init(from ikm: Data32) throws { + sk = blst_scalar() - secretKey = Data32(Data(out))! - publicKey = BLS.getPublicKey(secretKey) - } + // avoid capture self + withUnsafeMutablePointer(to: &sk) { sk in + ikm.data.withUnsafeBytes { ptr in + blst_keygen(sk, ptr.baseAddress, ikm.data.count, nil, 0) + } + } - public init(privateKey: Data32) throws { - var sk = blst_scalar() - blst_scalar_from_bendian(&sk, [UInt8](privateKey.data)) + var pk = blst_p1() + blst_sk_to_pk_in_g1(&pk, &sk) - guard blst_sk_check(&sk) else { - throw BLSError.blstError(BLST_BAD_SCALAR) + publicKey = PublicKey(pk: &pk) } - secretKey = privateKey - publicKey = BLS.getPublicKey(secretKey) - } + public func sign(message: Data) -> Data96 { + var msgHash = blst_p2() - private static func getPublicKey(_ secretKey: Data32) -> Data48 { - var sk = blst_scalar() - blst_scalar_from_bendian(&sk, [UInt8](secretKey.data)) + message.withUnsafeBytes { ptr in + blst_hash_to_g2(&msgHash, ptr.baseAddress, ptr.count, nil, 0, nil, 0) + } - var pk = blst_p1() - blst_sk_to_pk_in_g1(&pk, &sk) + var sig = blst_p2() + blst_sign_pk_in_g1(&sig, &msgHash, &sk) - var pkBytes = [UInt8](repeating: 0, count: 48) - blst_p1_compress(&pkBytes, &pk) + var sigBytes = Data(repeating: 0, count: 96) + sigBytes.withUnsafeMutableBytes { ptr in + blst_p2_compress(ptr.baseAddress, &sig) + } - return Data48(Data(pkBytes))! + return Data96(sigBytes)! + } } - public func sign(message: Data) -> Data96 { - var sk = blst_scalar() - blst_scalar_from_bendian(&sk, [UInt8](secretKey.data)) + public final class PublicKey: PublicKeyProtocol { + // this is immutable after initialization but C API requires it to be mutable + fileprivate var pk = blst_p1_affine() + public let data: Data48 - var msgHash = blst_p2() - blst_hash_to_g2(&msgHash, [UInt8](message), message.count, nil, 0, nil, 0) + fileprivate init(pk: inout blst_p1) { + var data = Data(repeating: 0, count: 48) - var sig = blst_p2() - blst_sign_pk_in_g1(&sig, &msgHash, &sk) + data.withUnsafeMutableBytes { ptr in + blst_p1_compress(ptr.baseAddress, &pk) + } - var sigBytes = [UInt8](repeating: 0, count: 96) - blst_p2_compress(&sigBytes, &sig) + blst_p1_to_affine(&self.pk, &pk) + self.data = Data48(data)! + } - return Data96(Data(sigBytes))! - } + public init(from data: Data48) throws { + self.data = data - public static func verify(signature: Data96, message: Data, publicKey: Data48) -> Bool { - var pk = blst_p1_affine() - var sig = blst_p2_affine() + try data.data.withUnsafeBytes { ptr in + let result = blst_p1_uncompress(&pk, ptr.baseAddress) + guard result == BLST_SUCCESS else { + throw Error.blstError(result) + } + } + } - let pkResult = blst_p1_uncompress(&pk, [UInt8](publicKey.data)) - let sigResult = blst_p2_uncompress(&sig, [UInt8](signature.data)) + public convenience init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data48.self) + try self.init(from: data) + } - guard pkResult == BLST_SUCCESS, sigResult == BLST_SUCCESS else { - return false + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(data) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(data) } - let verifyResult = blst_core_verify_pk_in_g1( - &pk, &sig, true, [UInt8](message), message.count, nil, 0, nil, 0 - ) + public static func == (lhs: PublicKey, rhs: PublicKey) -> Bool { + lhs.data == rhs.data + } + + public func verify(signature: Data96, message: Data) -> Bool { + var sig = blst_p2_affine() - return verifyResult == BLST_SUCCESS + let res = signature.data.withUnsafeBytes { ptr in + blst_p2_uncompress(&sig, ptr.baseAddress) + } + + guard res == BLST_SUCCESS else { + return false + } + + let verifyResult = message.withUnsafeBytes { ptr in + blst_core_verify_pk_in_g1( + &pk, &sig, true, ptr.baseAddress, ptr.count, nil, 0, nil, 0 + ) + } + + return verifyResult == BLST_SUCCESS + } } public static func aggregateVerify( - signature: Data96, messages: [Data], publicKeys: [Data48] - ) - -> Bool - { + signature: Data96, messages: [Data], publicKeys: [PublicKey] + ) -> Bool { + if messages.count != publicKeys.count { + return false + } + let size = blst_pairing_sizeof() let ctx = OpaquePointer(malloc(size)) + defer { free(UnsafeMutableRawPointer(ctx)) } blst_pairing_init(ctx, true, nil, 0) @@ -110,34 +139,28 @@ public struct BLS { return false } - for i in 0 ..< publicKeys.count { - var pk = blst_p1_affine() - let pkResult = blst_p1_uncompress(&pk, [UInt8](publicKeys[i].data)) - guard pkResult == BLST_SUCCESS else { - return false - } - - let aggregateResult: BLST_ERROR = - if i == 1 { + var first = true + for (key, message) in zip(publicKeys, messages) { + let res = message.withUnsafeBytes { ptr in + if first { blst_pairing_aggregate_pk_in_g1( - ctx, &pk, &sig, [UInt8](messages[i]), messages[i].count, nil, 0 + ctx, &key.pk, &sig, ptr.baseAddress, ptr.count, nil, 0 ) } else { blst_pairing_aggregate_pk_in_g1( - ctx, &pk, nil, [UInt8](messages[i]), messages[i].count, nil, 0 + ctx, &key.pk, nil, ptr.baseAddress, ptr.count, nil, 0 ) } - guard aggregateResult == BLST_SUCCESS else { + } + guard res == BLST_SUCCESS else { return false } + + first = false } blst_pairing_commit(ctx) - let result = blst_pairing_finalverify(ctx, nil) - - free(UnsafeMutableRawPointer(ctx)) - - return result + return blst_pairing_finalverify(ctx, nil) } public static func aggregateSignatures(signatures: [Data96]) throws -> Data96 { @@ -145,17 +168,23 @@ public struct BLS { for signature in signatures { var sig = blst_p2_affine() - let sigResult = blst_p2_uncompress(&sig, [UInt8](signature.data)) - guard sigResult == BLST_SUCCESS else { - throw BLSError.blstError(sigResult) + try signature.data.withUnsafeBytes { ptr in + let sigResult = blst_p2_uncompress(&sig, ptr.baseAddress) + guard sigResult == BLST_SUCCESS else { + throw Error.blstError(sigResult) + } + } + // silance the memory overlapping accessing warning + // as this is supported by the C API + withUnsafeMutablePointer(to: &aggregate) { ptr in + blst_p2_add_or_double_affine(ptr, ptr, &sig) } - var aggCopy = aggregate - blst_p2_add_or_double_affine(&aggregate, &aggCopy, &sig) } - var sigCompressed = [UInt8](repeating: 0, count: 96) - blst_p2_compress(&sigCompressed, &aggregate) - - return Data96(Data(sigCompressed))! + var sigCompressed = Data(repeating: 0, count: 96) + sigCompressed.withUnsafeMutableBytes { ptr in + blst_p2_compress(ptr.baseAddress, &aggregate) + } + return Data96(sigCompressed)! } } diff --git a/Utils/Sources/Utils/Crypto/Bandersnatch.swift b/Utils/Sources/Utils/Crypto/Bandersnatch.swift index 413614a6..ea7536f1 100644 --- a/Utils/Sources/Utils/Crypto/Bandersnatch.swift +++ b/Utils/Sources/Utils/Crypto/Bandersnatch.swift @@ -76,11 +76,10 @@ private func call( out = out2! } -public enum Bandersnatch { +public enum Bandersnatch: KeyType { public enum Error: Swift.Error { case createSecretFailed(Int) case createPublicKeyFailed(Int) - case invalidSeedLength case createRingContextFailed(Int) case ringVRFSignFailed(Int) case ietfVRFSignFailed(Int) @@ -90,18 +89,14 @@ public enum Bandersnatch { case ietfVRFVerifyFailed(Int) } - public class SecretKey { + public final class SecretKey: SecretKeyProtocol { fileprivate let ptr: OpaquePointer public let publicKey: PublicKey - public init(seed: Data) throws(Error) { - guard seed.count >= 32 else { - throw .invalidSeedLength - } - + public init(from seed: Data32) throws(Error) { var ptr: OpaquePointer! - try call(seed) { ptrs in + try call(seed.data) { ptrs in secret_new(ptrs[0].ptr, ptrs[0].count, &ptr) } onErr: { err throws(Error) in throw .createSecretFailed(err) @@ -139,7 +134,7 @@ public enum Bandersnatch { } } - public class PublicKey { + public final class PublicKey: PublicKeyProtocol, Hashable { fileprivate let ptr: OpaquePointer public let data: Data32 @@ -175,6 +170,25 @@ public enum Bandersnatch { public_free(ptr) } + public convenience init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data32.self) + try self.init(data: data) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(data) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(data) + } + + public static func == (lhs: PublicKey, rhs: PublicKey) -> Bool { + lhs.data == rhs.data + } + /// Non-Anonymous VRF signature verification. /// /// Used for ticket claim verification during block import. @@ -206,7 +220,7 @@ public enum Bandersnatch { } } - public class RingContext { + public final class RingContext { fileprivate let ptr: OpaquePointer public init(size: UInt) throws(Error) { @@ -224,7 +238,7 @@ public enum Bandersnatch { } } - public class Prover { + public final class Prover { private let secret: SecretKey private let ring: [PublicKey] private let ringPtrs: [OpaquePointer?] @@ -269,7 +283,7 @@ public enum Bandersnatch { } } - public class RingCommitment { + public final class RingCommitment { fileprivate let ptr: OpaquePointer public let data: Data144 @@ -318,7 +332,7 @@ public enum Bandersnatch { } } - public class Verifier { + public struct Verifier { private let ctx: RingContext private let commitment: RingCommitment diff --git a/Utils/Sources/Utils/Crypto/Ed25519.swift b/Utils/Sources/Utils/Crypto/Ed25519.swift index 25384ba9..aa183ec1 100644 --- a/Utils/Sources/Utils/Crypto/Ed25519.swift +++ b/Utils/Sources/Utils/Crypto/Ed25519.swift @@ -1,38 +1,57 @@ import Crypto import Foundation -public struct Ed25519 { - public let secretKey: Curve25519.Signing.PrivateKey +public enum Ed25519: KeyType { + public final class SecretKey: SecretKeyProtocol { + private let secretKey: Curve25519.Signing.PrivateKey + public let publicKey: PublicKey + + public init(from seed: Data32) throws { + secretKey = try Curve25519.Signing.PrivateKey(rawRepresentation: seed.data) + publicKey = PublicKey(pk: secretKey.publicKey) + } - public var publicKey: Data32 { - Data32(secretKey.publicKey.rawRepresentation)! + public func sign(message: Data) throws -> Data64 { + let signature = try secretKey.signature(for: message) + return Data64(signature)! + } } - public var privateKey: Data32 { - Data32(secretKey.rawRepresentation)! - } + public final class PublicKey: PublicKeyProtocol { + private let publicKey: Curve25519.Signing.PublicKey + public let data: Data32 - public init() { - secretKey = Curve25519.Signing.PrivateKey() - } + fileprivate init(pk: Curve25519.Signing.PublicKey) { + publicKey = pk + data = Data32(pk.rawRepresentation)! + } - public init?(privateKey: Data32) { - guard let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey.data) else { - return nil + public init(from data: Data32) throws { + publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: data.data) + self.data = data } - secretKey = key - } - public func sign(message: Data) throws -> Data64 { - let signature = try secretKey.signature(for: message) - return Data64(signature)! - } + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(data) + } + + public convenience init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data32.self) + try self.init(from: data) + } - public static func verify(signature: Data64, message: Data, publicKey: Data32) -> Bool { - guard let publicKey = try? Curve25519.Signing.PublicKey(rawRepresentation: publicKey.data) else { - return false + public func hash(into hasher: inout Hasher) { + hasher.combine(data) } - return publicKey.isValidSignature(signature.data, for: message) + public static func == (lhs: PublicKey, rhs: PublicKey) -> Bool { + lhs.data == rhs.data + } + + public func verify(signature: Data64, message: Data) -> Bool { + publicKey.isValidSignature(signature.data, for: message) + } } } diff --git a/Utils/Sources/Utils/Crypto/KeyStore.swift b/Utils/Sources/Utils/Crypto/KeyStore.swift new file mode 100644 index 00000000..220550bd --- /dev/null +++ b/Utils/Sources/Utils/Crypto/KeyStore.swift @@ -0,0 +1,47 @@ +import Foundation + +public protocol KeyStore { + func generate(_ type: K.Type) async throws -> K.SecretKey + func add(_ type: K.Type, seed: Data32) async throws -> K.SecretKey + func contains(publicKey: PK) async -> Bool + func get(_ type: K.Type, publicKey: K.SecretKey.PublicKey) async -> K.SecretKey? +} + +struct HashableKey: Hashable { + let value: any PublicKeyProtocol + + func hash(into hasher: inout Hasher) { + hasher.combine(value) + } + + static func == (lhs: HashableKey, rhs: HashableKey) -> Bool { + lhs.value.equals(rhs: rhs.value) + } +} + +public class InMemoryKeyStore { + private var keys: [HashableKey: any SecretKeyProtocol] = [:] + + public init() {} + + public func generate(_ type: K.Type) async throws -> K.SecretKey { + try await add(type, seed: Data32.random()) + } + + public func add(_ type: K.Type, seed: Data32) async throws -> K.SecretKey { + let secretKey = try type.SecretKey(from: seed) + let hashableKey = HashableKey(value: secretKey.publicKey) + keys[hashableKey] = secretKey + return secretKey + } + + public func contains(publicKey: some PublicKeyProtocol) async -> Bool { + let hashableKey = HashableKey(value: publicKey) + return keys[hashableKey] != nil + } + + public func get(_: K.Type, publicKey: K.SecretKey.PublicKey) async -> K.SecretKey? { + let hashableKey = HashableKey(value: publicKey) + return keys[hashableKey] as? K.SecretKey + } +} diff --git a/Utils/Sources/Utils/Crypto/KeyType.swift b/Utils/Sources/Utils/Crypto/KeyType.swift new file mode 100644 index 00000000..598f1a01 --- /dev/null +++ b/Utils/Sources/Utils/Crypto/KeyType.swift @@ -0,0 +1,21 @@ +public protocol PublicKeyProtocol: Codable, Hashable {} + +public protocol SecretKeyProtocol { + associatedtype PublicKey: PublicKeyProtocol + init(from seed: Data32) throws + + var publicKey: PublicKey { get } +} + +public protocol KeyType { + associatedtype SecretKey: SecretKeyProtocol +} + +extension PublicKeyProtocol { + public func equals(rhs: any PublicKeyProtocol) -> Bool { + guard let rhsValue = rhs as? Self else { + return false + } + return self == rhsValue + } +} diff --git a/Utils/Sources/Utils/FixedSizeData.swift b/Utils/Sources/Utils/FixedSizeData.swift index 0b4a28e9..9a554366 100644 --- a/Utils/Sources/Utils/FixedSizeData.swift +++ b/Utils/Sources/Utils/FixedSizeData.swift @@ -80,6 +80,19 @@ extension FixedSizeData: EncodedSize { } } +extension FixedSizeData { + public static func random() -> Self { + var data = Data(repeating: 0, count: T.value) + data.withUnsafeMutableBytes { ptr in + ptr.baseAddress!.withMemoryRebound(to: UInt8.self, capacity: T.value) { ptr in + arc4random_buf(ptr, T.value) + } + } + + return Self(data)! + } +} + public typealias Data32 = FixedSizeData public typealias Data48 = FixedSizeData public typealias Data64 = FixedSizeData diff --git a/Utils/Tests/UtilsTests/Crypto/BLSTests.swift b/Utils/Tests/UtilsTests/Crypto/BLSTests.swift index b3f8f1ea..b2d38eb5 100644 --- a/Utils/Tests/UtilsTests/Crypto/BLSTests.swift +++ b/Utils/Tests/UtilsTests/Crypto/BLSTests.swift @@ -5,35 +5,33 @@ import Testing @Suite struct BLSTests { @Test func BLSSignatureWorks() throws { - let bls = try BLS(ikm: Data("this is random high entropy ikm for key1".utf8)) + let bls = try BLS.SecretKey(from: Data32()) let publicKey1 = bls.publicKey let message1 = Data("test1".utf8) let signature1 = bls.sign(message: message1) #expect( - BLS.verify(signature: signature1, message: message1, publicKey: publicKey1) + publicKey1.verify(signature: signature1, message: message1) ) let invalidMessage = Data("testUnknown".utf8) #expect( - !BLS.verify(signature: signature1, message: invalidMessage, publicKey: publicKey1) + !publicKey1.verify(signature: signature1, message: invalidMessage) ) var invalidSignature = signature1.data invalidSignature.replaceSubrange(0 ... 1, with: [2, 3]) #expect( - !BLS.verify( - signature: Data96(invalidSignature)!, message: message1, publicKey: publicKey1 - ) + !publicKey1.verify(signature: Data96(invalidSignature)!, message: message1) ) - let bls2 = try BLS(ikm: Data("this is random high entropy ikm for key2".utf8)) + let bls2 = try BLS.SecretKey(from: Data32.random()) let publicKey2 = bls2.publicKey let message2 = Data("test2".utf8) let signature2 = bls2.sign(message: message2) #expect( - BLS.verify(signature: signature2, message: message2, publicKey: publicKey2) + publicKey2.verify(signature: signature2, message: message2) ) let aggSig = try BLS.aggregateSignatures(signatures: [signature1, signature2]) diff --git a/Utils/Tests/UtilsTests/Crypto/Ed25519Tests.swift b/Utils/Tests/UtilsTests/Crypto/Ed25519Tests.swift index 4c05acc3..16b47d66 100644 --- a/Utils/Tests/UtilsTests/Crypto/Ed25519Tests.swift +++ b/Utils/Tests/UtilsTests/Crypto/Ed25519Tests.swift @@ -5,24 +5,22 @@ import Testing @Suite struct Ed25519Tests { @Test func testEd25519Signature() throws { - let ed25519 = Ed25519() + let ed25519 = try Ed25519.SecretKey(from: Data32.random()) let publicKey = ed25519.publicKey let message = Data("test".utf8) let signature = try ed25519.sign(message: message) - #expect(Ed25519.verify(signature: signature, message: message, publicKey: publicKey)) + #expect(publicKey.verify(signature: signature, message: message)) let invalidMessage = Data("tests".utf8) #expect( - !Ed25519.verify(signature: signature, message: invalidMessage, publicKey: publicKey) + !publicKey.verify(signature: signature, message: invalidMessage) ) var invalidSignature = signature.data invalidSignature.replaceSubrange(0 ... 1, with: [10, 12]) #expect( - !Ed25519.verify( - signature: Data64(invalidSignature)!, message: message, publicKey: publicKey - ) + !publicKey.verify(signature: Data64(invalidSignature)!, message: message) ) } }