Skip to content

Instantly share code, notes, and snippets.

@alexbosworth
Last active December 18, 2023 02:31
Show Gist options
  • Save alexbosworth/3e4918a18f1417a31362f70c091b8451 to your computer and use it in GitHub Desktop.
Save alexbosworth/3e4918a18f1417a31362f70c091b8451 to your computer and use it in GitHub Desktop.
// Copyright (c) 2017 Alex Bosworth
// Copyright (c) 2017 Pieter Wuille
//
// 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.
//
import XCTest
typealias StringCharacterByte = UInt8
extension String {
func lastIndex(of string: String) -> Int? {
guard let range = self.range(of: string, options: .backwards) else { return nil }
return characters.distance(from: startIndex, to: range.lowerBound)
}
}
let CHARSET = byteConvert(string: "qpzry9x8gf2tvdw0s3jn54khce6mua7l")
let GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
func polymod(_ values: [Int]) -> Int {
return values.reduce(1) { chk, value in
let top = chk >> 25
return (Int()..<5).reduce((chk & 0x1ffffff) << 5 ^ value) { chk, i in
guard (top >> i) & 1 > Int() else { return chk }
return chk ^ GENERATOR[i]
}
}
}
func hrpExpand(_ hrp: [StringCharacterByte]) -> [StringCharacterByte] {
return (Int()..<hrp.count).map { hrp[$0] >> 5 } + [StringCharacterByte()] + (Int()..<hrp.count).map { hrp[$0] & 31 }
}
func verifyChecksum(hrp: [StringCharacterByte], data: [StringCharacterByte]) -> Bool {
return polymod((hrpExpand(hrp) + data).map { Int($0) }) == 1
}
func createChecksum(hrp: [StringCharacterByte], data: [StringCharacterByte]) -> [StringCharacterByte] {
let values = (hrpExpand(hrp) + data + Array(repeating: StringCharacterByte(), count: 6)).map { Int($0) }
let mod: Int = polymod(values) ^ 1
return (Int()..<6).map { (mod >> (5 * (5 - $0))) & 31 }.map { StringCharacterByte($0) }
}
func byteConvert(string: String) -> [StringCharacterByte] {
return string.characters.map { String($0).unicodeScalars.first?.value }.flatMap { $0 }.map { StringCharacterByte($0) }
}
func stringConvert(bytes: [StringCharacterByte]) -> String {
return bytes.reduce(String(), { $0 + String(format: "%c", $1)})
}
func encode(hrp: [StringCharacterByte], data: [StringCharacterByte]) -> String {
let checksum = createChecksum(hrp: hrp, data: data)
return stringConvert(bytes: hrp) + "1" + stringConvert(bytes: (data + checksum).map { CHARSET[Int($0)] })
}
enum DecodeBech32Error: Error {
case caseMixing
case inconsistentHrp
case invalidAddress
case invalidBits
case invalidCharacter(String)
case invalidChecksum
case invalidPayToHashLength
case invalidVersion
case missingSeparator
case missingVersion
var localizedDescription: String {
switch self {
case .caseMixing:
return "Mixed case characters are not allowed"
case .inconsistentHrp:
return "Internally inconsistent HRP"
case .invalidAddress:
return "Address is not a valid type"
case .invalidBits:
return "Bits are not valid"
case .invalidCharacter(let char):
return "Character \"\(char)\" is not valid"
case .invalidChecksum:
return "Checksum failed to verify data"
case .invalidPayToHashLength:
return "Unknown hash length for encoded output payload hash"
case .invalidVersion:
return "Invalid version number"
case .missingSeparator:
return "Missing address data separator"
case .missingVersion:
return "Missing address version"
}
}
}
func decode(bechString: String) throws -> (hrp: [StringCharacterByte], data: [StringCharacterByte]) {
let bechBytes = byteConvert(string: bechString)
guard !(bechBytes.contains() { $0 < 33 && $0 > 126 }) else { throw DecodeBech32Error.invalidCharacter(bechString) }
let hasLower = bechBytes.contains() { $0 >= 97 && $0 <= 122 }
let hasUpper = bechBytes.contains() { $0 >= 65 && $0 <= 90 }
if hasLower && hasUpper { throw DecodeBech32Error.caseMixing }
let bechString = bechString.lowercased()
guard let pos = bechString.lastIndex(of: "1") else { throw DecodeBech32Error.missingSeparator }
if pos < 1 || pos + 7 > bechString.characters.count || bechString.characters.count > 90 {
throw DecodeBech32Error.missingSeparator
}
let bechStringBytes = byteConvert(string: bechString)
let hrp = byteConvert(string: bechString.substring(to: bechString.index(bechString.startIndex, offsetBy: pos)))
let data: [StringCharacterByte] = try ((pos + 1)..<bechStringBytes.count).map { i in
guard let d = CHARSET.index(of: bechStringBytes[i]) else {
throw DecodeBech32Error.invalidCharacter(stringConvert(bytes: [bechStringBytes[i]]))
}
return Byte(d)
}
guard verifyChecksum(hrp: hrp, data: data) else { throw DecodeBech32Error.invalidChecksum }
return (hrp: hrp, data: Array(data[Int()..<data.count - 6]))
}
func convertbits(data: [StringCharacterByte], fromBits: Int, toBits: Int, pad: Bool) throws -> [StringCharacterByte] {
var acc = Int()
var bits = StringCharacterByte()
let maxv = (1 << toBits) - 1
let converted: [[Int]] = try data.map { value in
if (value < 0 || (StringCharacterByte(Int(value) >> fromBits)) != 0) {
throw DecodeBech32Error.invalidCharacter(stringConvert(bytes: [value]))
}
acc = (acc << fromBits) | Int(value)
bits += StringCharacterByte(fromBits)
var values = [Int]()
while bits >= StringCharacterByte(toBits) {
bits -= UInt8(toBits)
values += [(acc >> Int(bits)) & maxv]
}
return values
}
let padding = pad && bits > StringCharacterByte() ? [acc << (toBits - Int(bits)) & maxv] : []
if !pad && (bits >= StringCharacterByte(fromBits) || acc << (toBits - Int(bits)) & maxv > Int()) {
throw DecodeBech32Error.invalidBits
}
return ((converted.flatMap { $0 }) + padding).map { StringCharacterByte($0) }
}
func encode(hrp: [StringCharacterByte], version: UInt8, program: [UInt8]) throws -> String {
let address = try encode(hrp: hrp, data: [version] + convertbits(data: program, fromBits: 8, toBits: 5, pad: true))
// Confirm encoded address parses without error
let _ = try decodeAddress(hrp: hrp, address: address)
return address
}
func decodeAddress(hrp: [StringCharacterByte], address: String) throws -> (version: UInt8, program: [UInt8]) {
let decoded = try decode(bechString: address)
// Confirm decoded address matches expected type
guard stringConvert(bytes: decoded.hrp) == stringConvert(bytes: hrp) else { throw DecodeBech32Error.inconsistentHrp }
// Confirm version byte is present
guard let versionByte = decoded.data.first else { throw DecodeBech32Error.missingVersion }
// Confirm version byte is within the acceptable range
guard !decoded.data.isEmpty && versionByte <= 16 else { throw DecodeBech32Error.invalidVersion }
let program = try convertbits(data: Array(decoded.data[1..<decoded.data.count]), fromBits: 5, toBits: 8, pad: false)
// Confirm program is a valid length
guard program.count > 1 && program.count < 41 else { throw DecodeBech32Error.invalidAddress }
if versionByte == UInt8() {
// Confirm program is a known byte length (20 for pkhash, 32 for scripthash)
guard program.count == 20 || program.count == 32 else { throw DecodeBech32Error.invalidPayToHashLength }
}
return (version: versionByte, program: program)
}
func segwitScriptPubKey(version: UInt8, program: [UInt8]) -> [UInt8] {
return [version > UInt8() ? version + 0x50 : UInt8(), UInt8(program.count)] + program
}
class TestBech32: XCTestCase {
func testInvalidAddresses() {
let INVALID_ADDRESS = [
"tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty",
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5",
"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2",
"bc1rw5uspcuh",
"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90",
"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7",
"tb1pw508d6qejxtdg4y5r3zarqfsj6c3",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv",
]
INVALID_ADDRESS.forEach { test in
["bc", "tb"].forEach { type in
do {
let _ = try decodeAddress(hrp: byteConvert(string: type), address: test)
XCTFail("Expected invalid address: \(test)")
} catch {
return
}
}
}
}
func testChecksums() {
let VALID_CHECKSUM: [String] = [
"A12UEL5L",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j",
"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w"
]
do {
try VALID_CHECKSUM.forEach { test in
let _ = try decode(bechString: test)
}
} catch {
XCTFail(error.localizedDescription)
}
}
func testValidAddresses() {
let VALID_BC_ADDRESSES: [String: (decoded: [UInt8], type: String)] = [
"BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4": (
decoded: [
0x00, 0x14, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54,
0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6
],
type: "bc"
),
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7": (
decoded: [
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
],
type: "tb"
),
"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx": (
decoded: [
0x51, 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
],
type: "bc"
),
"BC1SW50QA3JX3S": (decoded: [0x60, 0x02, 0x75, 0x1e], type: "bc"),
"bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj": (
decoded: [
0x52, 0x10, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54,
0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23
],
type: "bc"
),
"tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy": (
decoded: [
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
],
type: "tb"
)
]
do {
try VALID_BC_ADDRESSES.forEach { address, result in
let scriptPubKey = result.decoded
let hrp = byteConvert(string: result.type)
let ret = try decodeAddress(hrp: hrp, address: address)
let output = segwitScriptPubKey(version: ret.version, program: ret.program)
XCTAssertEqual(output, scriptPubKey)
let recreated = try encode(hrp: hrp, version: ret.version, program: ret.program).lowercased()
XCTAssertEqual(recreated, address.lowercased())
}
} catch {
XCTFail(error.localizedDescription)
}
}
}
@alexbosworth
Copy link
Author

Updated for segwit script version byte, via @sipa

@ashchan
Copy link

ashchan commented Apr 15, 2019

bechBytes.contains() { $0 < 33 && $0 > 126 }

Shouldn't this be bechBytes.contains() { $0 < 33 || $0 > 126 }?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment