From e5e1353b7840dc19d10e023fe1b784cd5469ca8a Mon Sep 17 00:00:00 2001 From: Wade Wilson Date: Wed, 14 Feb 2018 00:23:15 +0300 Subject: [PATCH] Initial commit --- .gitignore | 4 + LICENSE | 21 +++ Package.swift | 22 +++ README.md | 21 +++ Sources/Bech32.swift | 183 +++++++++++++++++++++ Sources/SegwitAddrCoder.swift | 108 ++++++++++++ Tests/Bech32Tests/Bech32Tests.swift | 247 ++++++++++++++++++++++++++++ Tests/LinuxMain.swift | 6 + 8 files changed, 612 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/Bech32.swift create mode 100644 Sources/SegwitAddrCoder.swift create mode 100644 Tests/Bech32Tests/Bech32Tests.swift create mode 100644 Tests/LinuxMain.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02c0875 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06705c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2018 Evolution Group Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..79ccca9 --- /dev/null +++ b/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:4.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Bech32", + products: [ + .library( + name: "Bech32", + targets: ["Bech32"]), + ], + targets: [ + .target( + name: "Bech32", + dependencies: [], + path: "./Sources"), + .testTarget( + name: "Bech32Tests", + dependencies: ["Bech32"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7bc833 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Bech32 + +Base32 address format for native v0-16 witness outputs implementation + +[See BIP173 proposal on bitcoin repo](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) + +Inspired by Pieter Wuille C++ reference implementation + +### Install + +- Drop files into project +- Correct Bech32Tests `@testable import` statement + +### Check + +Swift Package Manager is needed to run tests and build .xcodeproj. +If SPM (v4) is installed then one can type: + + git clone https://github.com/0xDEADP00L/Bech32.git + cd Bech32 + swift test diff --git a/Sources/Bech32.swift b/Sources/Bech32.swift new file mode 100644 index 0000000..c8dab60 --- /dev/null +++ b/Sources/Bech32.swift @@ -0,0 +1,183 @@ +// +// Bech32.swift +// +// Created by Evolution Group Ltd on 12.02.2018. +// Copyright © 2018 Evolution Group Ltd. All rights reserved. +// + +// Base32 address format for native v0-16 witness outputs implementation +// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki +// Inspired by Pieter Wuille C++ implementation + +import Foundation + +/// Bech32 checksum implementation +public class Bech32 { + private let gen: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + /// Bech32 checksum delimiter + private let checksumMarker: String = "1" + /// Bech32 character set for encoding + private let encCharset: Data = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".data(using: .utf8)! + /// Bech32 character set for decoding + private let decCharset: [Int8] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + ] + + /// Find the polynomial with value coefficients mod the generator as 30-bit. + private func polymod(_ values: Data) -> UInt32 { + var chk: UInt32 = 1 + for v in values { + let top = (chk >> 25) + chk = (chk & 0x1ffffff) << 5 ^ UInt32(v) + for i: UInt8 in 0..<5 { + chk ^= ((top >> i) & 1) == 0 ? 0 : gen[Int(i)] + } + } + return chk + } + + /// Expand a HRP for use in checksum computation. + private func expandHrp(_ hrp: String) -> Data { + guard let hrpBytes = hrp.data(using: .utf8) else { return Data() } + var result = Data(repeating: 0x00, count: hrpBytes.count*2+1) + for (i, c) in hrpBytes.enumerated() { + result[i] = c >> 5 + result[i + hrpBytes.count + 1] = c & 0x1f + } + result[hrp.count] = 0 + return result + } + + /// Verify checksum + private func verifyChecksum(hrp: String, checksum: Data) -> Bool { + var data = expandHrp(hrp) + data.append(checksum) + return polymod(data) == 1 + } + + /// Create checksum + private func createChecksum(hrp: String, values: Data) -> Data { + var enc = expandHrp(hrp) + enc.append(values) + enc.append(Data(repeating: 0x00, count: 6)) + let mod: UInt32 = polymod(enc) ^ 1 + var ret: Data = Data(repeating: 0x00, count: 6) + for i in 0..<6 { + ret[i] = UInt8((mod >> (5 * (5 - i))) & 31) + } + return ret + } + + /// Encode Bech32 string + public func encode(_ hrp: String, values: Data) -> String { + let checksum = createChecksum(hrp: hrp, values: values) + var combined = values + combined.append(checksum) + guard let hrpBytes = hrp.data(using: .utf8) else { return "" } + var ret = hrpBytes + ret.append("1".data(using: .utf8)!) + for i in combined { + ret.append(encCharset[Int(i)]) + } + return String(data: ret, encoding: .utf8) ?? "" + } + + /// Decode Bech32 string + public func decode(_ str: String) throws -> (hrp: String, checksum: Data) { + guard let strBytes = str.data(using: .utf8) else { + throw DecodingError.nonUTF8String + } + guard strBytes.count <= 90 else { + throw DecodingError.stringLengthExceeded + } + var lower: Bool = false + var upper: Bool = false + for c in strBytes { + // printable range + if c < 33 || c > 126 { + throw DecodingError.nonPrintableCharacter + } + // 'a' to 'z' + if c >= 97 && c <= 122 { + lower = true + } + // 'A' to 'Z' + if c >= 65 && c <= 90 { + upper = true + } + } + if lower && upper { + throw DecodingError.invalidCase + } + guard let pos = str.range(of: checksumMarker, options: .backwards)?.lowerBound else { + throw DecodingError.noChecksumMarker + } + let intPos: Int = str.distance(from: str.startIndex, to: pos) + guard intPos >= 1 else { + throw DecodingError.incorrectHrpSize + } + guard intPos + 7 <= str.count else { + throw DecodingError.incorrectChecksumSize + } + let vSize: Int = str.count - 1 - intPos + var values: Data = Data(repeating: 0x00, count: vSize) + for i in 0.. Data { + var acc: Int = 0 + var bits: Int = 0 + let maxv: Int = (1 << to) - 1 + let maxAcc: Int = (1 << (from + to - 1)) - 1 + var odata = Data() + for ibyte in idata { + acc = ((acc << from) | Int(ibyte)) & maxAcc + bits += from + while bits >= to { + bits -= to + odata.append(UInt8((acc >> bits) & maxv)) + } + } + if pad { + if bits != 0 { + odata.append(UInt8((acc << (to - bits)) & maxv)) + } + } else if (bits >= from || ((acc << (to - bits)) & maxv) != 0) { + throw CoderError.bitsConversionFailed + } + return odata + } + + /// Decode segwit address + public func decode(hrp: String, addr: String) throws -> (version: Int, program: Data) { + let dec = try bech32.decode(addr) + guard dec.hrp == hrp else { + throw CoderError.hrpMismatch(dec.hrp, hrp) + } + guard dec.checksum.count >= 1 else { + throw CoderError.checksumSizeTooLow + } + let conv = try convertBits(from: 5, to: 8, pad: false, idata: dec.checksum.advanced(by: 1)) + guard conv.count >= 2 && conv.count <= 40 else { + throw CoderError.dataSizeMismatch(conv.count) + } + guard dec.checksum[0] <= 16 else { + throw CoderError.segwitVersionNotSupported(dec.checksum[0]) + } + if dec.checksum[0] == 0 && conv.count != 20 && conv.count != 32 { + throw CoderError.segwitV0ProgramSizeMismatch(conv.count) + } + return (Int(dec.checksum[0]), conv) + } + + /// Encode segwit address + public func encode(hrp: String, version: Int, program: Data) throws -> String { + var enc = Data([UInt8(version)]) + enc.append(try convertBits(from: 8, to: 5, pad: true, idata: program)) + let result = bech32.encode(hrp, values: enc) + guard let _ = try? decode(hrp: hrp, addr: result) else { + throw CoderError.encodingCheckFailed + } + return result + } +} + +extension SegwitAddrCoder { + public enum CoderError: LocalizedError { + case bitsConversionFailed + case hrpMismatch(String, String) + case checksumSizeTooLow + + case dataSizeMismatch(Int) + case segwitVersionNotSupported(UInt8) + case segwitV0ProgramSizeMismatch(Int) + + case encodingCheckFailed + + var localizedDescription: String { + switch self { + case .bitsConversionFailed: + return "Failed to perform bits conversion" + case .checksumSizeTooLow: + return "Checksum size is too low" + case .dataSizeMismatch(let size): + return "Program size \(size) does not meet required range 2...40" + case .encodingCheckFailed: + return "Failed to check result after encoding" + case .hrpMismatch(let got, let expected): + return "Human-readable-part \"\(got)\" does not match requested \"\(expected)\"" + case .segwitV0ProgramSizeMismatch(let size): + return "Segwit program size \(size) does not meet version 0 requirments" + case .segwitVersionNotSupported(let version): + return "Segwit version \(version) is not supported by this decoder" + } + } + } +} diff --git a/Tests/Bech32Tests/Bech32Tests.swift b/Tests/Bech32Tests/Bech32Tests.swift new file mode 100644 index 0000000..adc92eb --- /dev/null +++ b/Tests/Bech32Tests/Bech32Tests.swift @@ -0,0 +1,247 @@ +// +// Bech32Tests.swift +// +// Created by Evolution Group Ltd on 12.02.2018. +// Copyright © 2018 Evolution Group Ltd. All rights reserved. +// + +// Base32 address format for native v0-16 witness outputs implementation +// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki +// Inspired by Pieter Wuille C++ implementation + +import XCTest +@testable import Bech32 + +fileprivate typealias InvalidChecksum = (bech32: String, error: Bech32.DecodingError) +fileprivate typealias ValidAddressData = (address: String, script: [UInt8]) +fileprivate typealias InvalidAddressData = (hrp: String, version: Int, programLen: Int) + +fileprivate extension Data { + var hex: String { + return self.map { String(format: "%02hhx", $0) }.joined() + } +} + +class Bech32Tests: XCTestCase { + + private let _validChecksum: [String] = [ + "A12UEL5L", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + "?1ezyfcl" + ] + + private let _invalidChecksum: [InvalidChecksum] = [ + (" 1nwldj5", Bech32.DecodingError.nonPrintableCharacter), + ("\u{7f}1axkwrx", Bech32.DecodingError.nonPrintableCharacter), + ("an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", Bech32.DecodingError.stringLengthExceeded), + ("pzry9x0s0muk", Bech32.DecodingError.noChecksumMarker), + ("1pzry9x0s0muk", Bech32.DecodingError.incorrectHrpSize), + ("x1b4n0q5v", Bech32.DecodingError.invalidCharacter), + ("li1dgmt3", Bech32.DecodingError.incorrectChecksumSize), + ("de1lg7wt\u{ff}", Bech32.DecodingError.nonPrintableCharacter), + ("10a06t8", Bech32.DecodingError.incorrectHrpSize), + ("1qzzfhee", Bech32.DecodingError.incorrectHrpSize) + ] + + private let _invalidAddress: [String] = [ + "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu" + ] + + private let _validAddressData: [ValidAddressData] = [ + ("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", [ + 0x00, 0x14, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, + 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6 + ]), + ("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", [ + 0x00, 0x20, 0x18, 0x63, 0x14, 0x3c, 0x14, 0xc5, 0x16, 0x68, 0x04, + 0xbd, 0x19, 0x20, 0x33, 0x56, 0xda, 0x13, 0x6c, 0x98, 0x56, 0x78, + 0xcd, 0x4d, 0x27, 0xa1, 0xb8, 0xc6, 0x32, 0x96, 0x04, 0x90, 0x32, + 0x62 + ]), + ("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", [ + 0x81, 0x28, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, + 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6, + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, 0x1c, + 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6 + ]), + ("BC1SW50QA3JX3S", [ + 0x90, 0x02, 0x75, 0x1e + ]), + ("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", [ + 0x82, 0x10, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, + 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23 + ]), + ("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", [ + 0x00, 0x20, 0x00, 0x00, 0x00, 0xc4, 0xa5, 0xca, 0xd4, 0x62, 0x21, + 0xb2, 0xa1, 0x87, 0x90, 0x5e, 0x52, 0x66, 0x36, 0x2b, 0x99, 0xd5, + 0xe9, 0x1c, 0x6c, 0xe2, 0x4d, 0x16, 0x5d, 0xab, 0x93, 0xe8, 0x64, + 0x33 + ]) + ] + + private let _invalidAddressData: [InvalidAddressData] = [ + ("BC", 0, 20), + ("bc", 0, 21), + ("bc", 17, 32), + ("bc", 1, 1), + ("bc", 16, 41) + ] + + let bech32 = Bech32() + let addrCoder = SegwitAddrCoder() + + func testValidChecksum() { + for valid in _validChecksum { + do { + let decoded = try bech32.decode(valid) + XCTAssertFalse(decoded.hrp.isEmpty, "Empty result for \"\(valid)\"") + let recoded = bech32.encode(decoded.hrp, values: decoded.checksum) + XCTAssert(valid.lowercased() == recoded.lowercased(), "Roundtrip encoding failed: \(valid) != \(recoded)") + } catch { + XCTFail("Error decoding \(valid): \(error.localizedDescription)") + } + } + } + + func testInvalidChecksum() { + for invalid in _invalidChecksum { + let checksum = invalid.bech32 + let reason = invalid.error + do { + let decoded = try bech32.decode(checksum) + XCTFail("Successfully decoded an invalid checksum \(checksum): \(decoded.checksum.hex)") + } catch let error as Bech32.DecodingError { + XCTAssert(errorsEqual(error, reason), "Decoding error mismatch, got \(error.localizedDescription), expected \(reason.localizedDescription)") + } catch { + XCTFail("Invalid error occured: \(error.localizedDescription)") + } + } + } + + func testValidAddress() { + for valid in _validAddressData { + let address = valid.address + let script = Data(valid.script) + var hrp = "bc" + + var decoded = try? addrCoder.decode(hrp: hrp, addr: address) + + do { + if decoded == nil { + hrp = "tb" + decoded = try addrCoder.decode(hrp: hrp, addr: address) + } + } catch { + XCTFail("Failed to decode \(address)") + continue + } + + let scriptPk = segwitPubKey(version: decoded!.version, program: decoded!.program) + XCTAssert(scriptPk == script, "Decoded script mismatch: \(scriptPk.hex) != \(script.hex)") + + do { + let recoded = try addrCoder.encode(hrp: hrp, version: decoded!.version, program: decoded!.program) + XCTAssertFalse(recoded.isEmpty, "Recoded string is empty for \(address)") + } catch { + XCTFail("Roundtrip encoding failed for \"\(address)\" with error: \(error.localizedDescription)") + } + } + } + + func testInvalidAddress() { + for invalid in _invalidAddress { + do { + let decoded = try addrCoder.decode(hrp: "bc", addr: invalid) + XCTFail("Successfully decoded an invalid address \(invalid) for hrp \"bc\": \(decoded.program.hex)") + } catch { + // OK here :) + } + + do { + let decoded = try addrCoder.decode(hrp: "tb", addr: invalid) + XCTFail("Successfully decoded an invalid address \(invalid) for hrp \"tb\": \(decoded.program.hex)") + } catch { + // OK again + } + } + } + + func testInvalidAddressEncoding() { + for invalid in _invalidAddressData { + do { + let zeroData = Data(repeating: 0x00, count: invalid.programLen) + let wtf = try addrCoder.encode(hrp: invalid.hrp, version: invalid.version, program: zeroData) + XCTFail("Successfully encoded zero bytes data \(wtf)") + } catch { + // the way it should go + } + } + } + + func testAddressEncodingDecodingPerfomance() { + let addressToCode = _validAddressData[0].address + self.measure { + do { + for _ in 0..<10 { + let decoded = try addrCoder.decode(hrp: "bc", addr: addressToCode) + let _ = try addrCoder.encode(hrp: "bc", version: decoded.version, program: decoded.program) + } + } catch { + XCTFail(error.localizedDescription) + return + } + } + } + + private func segwitPubKey(version: Int, program: Data) -> Data { + var result = Data() + result.append(version != 0 ? (0x80 | UInt8(version)) : 0x00) + result.append(UInt8(program.count)) + result.append(program) + return result + } + + private func errorsEqual(_ lhs: Bech32.DecodingError, _ rhs: Bech32.DecodingError) -> Bool { + switch lhs { + case .checksumMismatch: + return rhs == .checksumMismatch + case .incorrectChecksumSize: + return rhs == .incorrectChecksumSize + case .incorrectHrpSize: + return rhs == .incorrectHrpSize + case .invalidCase: + return rhs == .invalidCase + case .invalidCharacter: + return rhs == .invalidCharacter + case .noChecksumMarker: + return rhs == .noChecksumMarker + case .nonUTF8String: + return rhs == .nonUTF8String + case .stringLengthExceeded: + return rhs == .stringLengthExceeded + case .nonPrintableCharacter: + return rhs == .nonPrintableCharacter + } + } + + static var allTests = [ + ("Valid Checksum", testValidChecksum), + ("Invalid Checksum", testInvalidChecksum), + ("Valid Address", testValidAddress), + ("Invalid Address", testInvalidAddress), + ("Zero Data", testInvalidAddressEncoding), + ("Perfomance", testAddressEncodingDecodingPerfomance) + ] +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..2e00678 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,6 @@ +import XCTest +@testable import Bech32Tests + +XCTMain([ + testCase(Bech32Tests.allTests), +])