Initial commit

This commit is contained in:
Wade Wilson 2018-02-14 00:23:15 +03:00
commit e5e1353b78
8 changed files with 612 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
/.build
/Packages
/*.xcodeproj

21
LICENSE Normal file
View File

@ -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.

22
Package.swift Normal file
View File

@ -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"]),
]
)

21
README.md Normal file
View File

@ -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

183
Sources/Bech32.swift Normal file
View File

@ -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..<vSize {
let c = strBytes[i + intPos + 1]
let decInt = decCharset[Int(c)]
if decInt == -1 {
throw DecodingError.invalidCharacter
}
values[i] = UInt8(decInt)
}
let hrp = String(str[..<pos]).lowercased()
guard verifyChecksum(hrp: hrp, checksum: values) else {
throw DecodingError.checksumMismatch
}
return (hrp, Data(values[..<(vSize-6)]))
}
}
extension Bech32 {
public enum DecodingError: LocalizedError {
case nonUTF8String
case nonPrintableCharacter
case invalidCase
case noChecksumMarker
case incorrectHrpSize
case incorrectChecksumSize
case stringLengthExceeded
case invalidCharacter
case checksumMismatch
var localizedDescription: String {
switch self {
case .checksumMismatch:
return "Checksum doesn't match"
case .incorrectChecksumSize:
return "Checksum size too low"
case .incorrectHrpSize:
return "Human-readable-part is too small or empty"
case .invalidCase:
return "String contains mixed case characters"
case .invalidCharacter:
return "Invalid character met on decoding"
case .noChecksumMarker:
return "Checksum delimiter not found"
case .nonPrintableCharacter:
return "Non printable character in input string"
case .nonUTF8String:
return "String cannot be decoded by utf8 decoder"
case .stringLengthExceeded:
return "Input string is too long"
}
}
}
}

View File

@ -0,0 +1,108 @@
//
// SegwitAddrCoder.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
/// Segregated Witness Address encoder/decoder
public class SegwitAddrCoder {
private let bech32 = Bech32()
/// Convert from one power-of-2 number base to another
private func convertBits(from: Int, to: Int, pad: Bool, idata: Data) throws -> 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"
}
}
}
}

View File

@ -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)
]
}

6
Tests/LinuxMain.swift Normal file
View File

@ -0,0 +1,6 @@
import XCTest
@testable import Bech32Tests
XCTMain([
testCase(Bech32Tests.allTests),
])