Skip to content

Commit

Permalink
📖 Add documentation for Base58Check and Bech32
Browse files Browse the repository at this point in the history
  • Loading branch information
usatie committed Sep 21, 2019
1 parent 57f4f24 commit 10aff45
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension BitcoinAddress {
assertionFailure("cashaddr is only supported for \(network).")
scheme = .bitcoincash
}
return Bech32.encode([versionByte.rawValue] + data, prefix: scheme.rawValue)
return Bech32.encode(payload: [versionByte.rawValue] + data, prefix: scheme.rawValue)
}

/// Creates a new BitcoinAddress with the bech32 encoded address with scheme.
Expand Down
31 changes: 21 additions & 10 deletions Sources/BitcoinKit/Core/Address/Encoding/Base58Check.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,40 @@

import Foundation

/// Version = 1 byte of 0 (zero); on the test network, this is 1 byte of 111
/// Key hash = Version concatenated with RIPEMD-160(SHA-256(public key))
/// Checksum = 1st 4 bytes of SHA-256(SHA-256(Key hash))
/// Bitcoin Address = Base58Encode(Key hash concatenated with Checksum)
/// Base58 encoding/decoding with checksum verification
/// A set of Base58Check coding methods.
///
/// ```
/// // Encode bytes to address
/// let address = Base58Check.encode([versionByte] + pubkeyHash)
///
/// // Decode address to bytes
/// guard let payload = Base58Check.decode(address) else {
/// // Possible cause 1: Coding format is invalid
/// // Possible cause 2: Checksum is invalid
/// // Invalid checksum or Base58 coding
/// throw SomeError()
/// }
/// let versionByte = payload[0]
/// let pubkeyHash = payload.dropFirst()
/// ```
public struct Base58Check {
public static func encode(_ bytes: Data) -> String {
let checksum: Data = Crypto.sha256sha256(bytes).prefix(4)
return Base58.encode(bytes + checksum)
/// Encodes the data to Base58Check encoded string
///
/// Puts checksum bytes to the original data and then, encode the combined
/// data to Base58 string.
/// ```
/// let address = Base58Check.encode([versionByte] + pubkeyHash)
/// ```
public static func encode(_ payload: Data) -> String {
let checksum: Data = Crypto.sha256sha256(payload).prefix(4)
return Base58.encode(payload + checksum)
}

/// Decode the Base58 encoded String value to original payload
///
/// First validate if checksum bytes are the first 4 bytes of the sha256(sha256(payload)).
/// If it's valid, returns the original payload.
/// ```
/// let payload = Base58Check.decode(base58checkText)
/// ```
public static func decode(_ string: String) -> Data? {
guard let raw = Base58.decode(string) else {
return nil
Expand Down
67 changes: 59 additions & 8 deletions Sources/BitcoinKit/Core/Address/Encoding/Bech32.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,78 @@

import Foundation

/// A set of Bech32 coding methods.
///
/// ```
/// // Encode bytes to address
/// let cashaddr: String = Bech32.encode(payload: [versionByte] + pubkeyHash,
/// prefix: "bitcoincash")
///
/// // Decode address to bytes
/// guard let payload: Data = Bech32.decode(text: address) else {
/// // Invalid checksum or Bech32 coding
/// throw SomeError()
/// }
/// let versionByte = payload[0]
/// let pubkeyHash = payload.dropFirst()
/// ```
public struct Bech32 {
internal static let base32Alphabets = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

public static func encode(_ bytes: Data, prefix: String, seperator: String = ":") -> String {
let payload = convertTo5bit(data: bytes, pad: true)
let checksum: Data = createChecksum(prefix: prefix, payload: payload) // Data of [UInt5]
let combined: Data = payload + checksum // Data of [UInt5]
/// Encodes the data to Bech32 encoded string
///
/// Creates checksum bytes from the prefix and the payload, and then puts the
/// checksum bytes to the original data. Then, encode the combined data to
/// Base32 string. At last, returns the combined string of prefix, separator
/// and the encoded base32 text.
/// ```
/// let address = Base58Check.encode(payload: [versionByte] + pubkeyHash,
/// prefix: "bitcoincash")
/// ```
/// - Parameters:
/// - payload: The data to encode
/// - prefix: The prefix of the encoded text. It is also used to create checksum.
/// - separator: separator that separates prefix and Base32 encoded text
public static func encode(payload: Data, prefix: String, separator: String = ":") -> String {
let payloadUint5 = convertTo5bit(data: payload, pad: true)
let checksumUint5: Data = createChecksum(prefix: prefix, payload: payloadUint5) // Data of [UInt5]
let combined: Data = payloadUint5 + checksumUint5 // Data of [UInt5]
var base32 = ""
for b in combined {
let index = String.Index(utf16Offset: Int(b), in: base32Alphabets)
base32 += String(base32Alphabets[index])
}

return prefix + seperator + base32
return prefix + separator + base32
}

// string : "bitcoincash:qql8zpwglr3q5le9jnjxkmypefaku39dkygsx29fzk"
public static func decode(_ string: String, seperator: String = ":") -> (prefix: String, data: Data)? {
@available(*, unavailable, renamed: "encode(payload:prefix:separator:)")
public static func encode(_ bytes: Data, prefix: String, seperator: String = ":") -> String {
return encode(payload: bytes, prefix: prefix, separator: seperator)
}

/// Decodes the Bech32 encoded string to original payload
///
/// ```
/// // Decode address to bytes
/// guard let payload: Data = Bech32.decode(text: address) else {
/// // Invalid checksum or Bech32 coding
/// throw SomeError()
/// }
/// let versionByte = payload[0]
/// let pubkeyHash = payload.dropFirst()
/// ```
/// - Parameters:
/// - string: The data to encode
/// - separator: separator that separates prefix and Base32 encoded text
public static func decode(_ string: String, separator: String = ":") -> (prefix: String, data: Data)? {
// We can't have empty string.
// Bech32 should be uppercase only / lowercase only.
guard !string.isEmpty && [string.lowercased(), string.uppercased()].contains(string) else {
return nil
}

let components = string.components(separatedBy: seperator)
let components = string.components(separatedBy: separator)
// We can only handle string contains both scheme and base32
guard components.count == 2 else {
return nil
Expand Down Expand Up @@ -76,6 +123,10 @@ public struct Bech32 {
}
return (prefix, Data(bytes))
}
@available(*, unavailable, renamed: "decode(string:separator:)")
public static func decode(_ string: String, seperator: String = ":") -> (prefix: String, data: Data)? {
return decode(string, separator: seperator)
}

internal static func verifyChecksum(prefix: String, payload: Data) -> Bool {
return PolyMod(expand(prefix) + payload) == 0
Expand Down
2 changes: 1 addition & 1 deletion Tests/BitcoinKitTests/Core/Address/Bech32Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class Bech32Tetst: XCTestCase {
func HexEncodesToBech32(hex: String, prefix: String, bech32: String, versionByte: UInt8) {
//Encode
let data = Data(hex: hex)!
XCTAssertEqual(Bech32.encode(Data([versionByte]) + data, prefix: prefix), bech32)
XCTAssertEqual(Bech32.encode(payload: Data([versionByte]) + data, prefix: prefix), bech32)
//Decode
let data2 = Bech32.decode(bech32)!
XCTAssertEqual(data2.prefix, prefix)
Expand Down

0 comments on commit 10aff45

Please sign in to comment.