SwiftBSV
A pure-Swift SDK for Bitcoin SV. SwiftBSV powers the wallet inside Henceforth and ships as a standalone Swift Package for any iOS or macOS app that needs Bitcoin keys, transactions, scripts, or SPV verification. Primitive types (PrivateKey, PublicKey, Address, Bip32, SPV models, BEEF/BUMP) are Sendable; mutable builder and script types are not.
About
Links to Source Code
github.com/henryhudson/SwiftBSV
Add it to a Swift package via the dependency stanza shown in the Integration chapter — one line in Package.swift or one menu click in Xcode.
Status
- Swift tools 5.9+, builds cleanly under Swift 6 with strict concurrency.
- Sendable for all value-type primitives —
Address,PrivateKey,PublicKey,Point,Bip32,Network,BInt, all SPV models,BEEF,BUMP. Mutable types (TxBuilder,Transaction,Script) are notSendable— pass the finalTransaction.serialized()Dataacross actor boundaries instead. - No deprecated dependencies. CryptoSwift 1.9.x and Boilertalk's secp256k1 binding are the only third-party packages.
- CI green on macOS 15 + Xcode 16 via GitHub Actions on every push to
main.
What This Book Covers
The chapters that follow walk every public type in the SDK from the bottom up:
- Cryptographic primitives (Point, BInt, Crypto)
- Keys and addresses (PrivateKey, PublicKey, Address, WIF)
- HD wallets (Bip39, Bip32) and Type42 (BRC-42) invoice-numbered derivation
- Signatures (ECDSA, sighash, BSM) and ECIES encryption (BIE1 wire format)
- Scripts and the full BSV opcode set
- Transactions and the fluent
TxBuilder - Simplified Payment Verification (block headers + Merkle proofs)
- SwiftPM integration and contributing
Goals
What SwiftBSV Is
- Idiomatic Swift. Value types where they make sense (
TxBuilderis astruct;Bip39is aclass). NoNSprefixes leaking through, no implicit globals, no completion-handler closures whereasync/awaitwould do. - Compile-time safe. Optionals where failure is part of the contract (parsing a malformed WIF, decoding a truncated DER signature). Throwing functions where the caller must handle it. No
try!, nofatalErrorin the production path. - Wire-compatible with the rest of BSV. ECIES emits
BIE1-format ciphertext readable by@bsv/sdk. BSM signatures verify against any other Bitcoin Signed Message implementation. Transaction binary matches whatever a node will accept. - Small surface, deep semantics. Every public type has a single clear responsibility. The whole library fits in your head once you've read this book.
What SwiftBSV Is Not
- Not a wallet. Wallet UX, key storage, address discovery, sync strategy — those live one layer up. SwiftBSV gives you the primitives; Henceforth shows one way to compose them.
- Not a network client. Broadcasting, fee quoting, UTXO fetching, Merkle-proof retrieval — all out of scope. SwiftBSV validates a Merkle proof; fetching one over HTTPS is your problem.
- Not a token/ordinal SDK. BSV-20, BSV-21, 1Sat Ordinals, BAP, MAP — all build on top of the primitives here. SwiftBSV stays at the protocol layer.
Future
- Streaming Merkle-tree construction for very large blocks.
- More extensive opcode tests — the Hayes-style suite that covers Henceforth's FORTH side has no exact analogue here yet.
- A formal benchmark suite tracking ECDSA verification and SHA-256 throughput across iPhone generations.
Credits
Will Townsend — Original SwiftBSV
SwiftBSV was originally written by Will Townsend at wtsnz/SwiftBSV as a Swift port of the moneybutton/bsv JavaScript library. The current fork at henryhudson/SwiftBSV continues from that foundation: BSV-specific protocol decisions (restored opcodes, FORKID sighash, no segwit), Swift 6 strict concurrency, defensive Data slicing throughout the public surface, ECIES BIE1 wire compatibility, and a SPV verification module.
Boilertalk's secp256k1.swift
Boilertalk's secp256k1.swift wraps Pieter Wuille's reference C library (libsecp256k1) for Swift. SwiftBSV uses it for every elliptic-curve operation: ECDSA sign and verify, public-key tweak (Type42), compact signature recovery (BSM).
CryptoSwift
Marcin Krzyżanowski's CryptoSwift provides SHA-256, SHA-512, RIPEMD-160, AES-CBC, HMAC, and PBKDF2. Apple's CryptoKit covers most modern needs but lacks RIPEMD-160 (used in Hash160 for address derivation) and AES-CBC (used in ECIES BIE1).
Bitcoin White Paper
The reference for everything in this book. Section 8 in particular — Simplified Payment Verification — defines the trust model implemented in the SPV chapter.
Henceforth
The companion book Henceforth documents the iOS app that consumes this SDK.
Cryptographic Primitives
BInt — Big Integer
Bitcoin keys are 256-bit integers. Swift's Int tops out at 64 bits, so SwiftBSV ships its own arbitrary-precision integer type, BInt, modeled on the conventional sign-magnitude representation. It conforms to Comparable, Equatable, Hashable, and the standard arithmetic operators.
let n = BInt("01abcdef", radix: 16)! // hex literal
let m = BInt("10000000000000000000000")! // decimal string
let sum = n + m // operator overloading
let neg = -m // unary minus
if n < m { print("n is smaller") }You almost never write BInt directly in app code. The library uses it internally for the curve order N, the integer form of a private key, and the components of an ECDSA signature.
Point — A Point on secp256k1
Point represents an (x, y) coordinate on the secp256k1 curve. It is the mathematical content of a public key.
let pubkey: PublicKey = privateKey.publicKey
let point: Point = pubkey.point // x: BInt, y: BInt
// Compressed serialization (33 bytes)
let compressed: Data = pubkey.toBuffer()
// DER serialization (33 or 65 bytes)
let der: Data = pubkey.toDer(compressed: true)You only handle Point directly in two places: when you write your own opcode-level script that pushes a curve point, or when you implement a multi-signature scheme by hand. For ordinary signing, encryption, and address derivation, PublicKey is the right type.
Crypto — Hash and HMAC Helpers
The Crypto final class bundles the hash functions Bitcoin uses, plus an HMAC helper. Static functions only — no state, no instance.
let bytes: Data = "hello".data(using: .utf8)!
let sha256: Data = Crypto.sha256(bytes) // 32 bytes
let sha256d: Data = Crypto.sha256sha256(bytes) // 32 bytes (BTC standard)
let hash160: Data = Crypto.sha256ripemd160(bytes) // 20 bytes (address hash)
let sha512: Data = Crypto.sha512(bytes) // 64 bytes
let key: Data = "secret".data(using: .utf8)!
let mac: Data = Crypto.hmacsha512(key: key, data: bytes) // 64 bytesverifySignature
A general DER-signature verifier:
let message: Data = ... // 32-byte hash (pre-computed by caller)
let sig: Data = ... // DER-encoded signature
let pub: PublicKey = ...
let ok: Bool = Crypto.verifySignature(
sig,
message: message,
publicKey: pub
)The function verifies the bytes as given — it does not double-SHA-256 the input. Pass a pre-computed hash. For the Bitcoin transaction-signature variant that strips the sighash byte and constructs the digest from the transaction context, use Crypto.verifySigData(for:inputIndex:utxo:sigData:pubKeyData:) directly.
Keys and Addresses
Network
Everything in SwiftBSV that has a wire-format byte representation carries a Network enum so the same code can produce mainnet and testnet artifacts:
public enum Network: Sendable {
case mainnet
case testnet
}The default everywhere is .mainnet. Pass .testnet explicitly when you mean it.
PrivateKey
A PrivateKey wraps a 256-bit secret integer (BInt), a network tag, and a compression flag.
// Random key for the given network
let key = PrivateKey(network: .mainnet)
// From 32-byte raw seed
let key2 = PrivateKey(data: bytes, network: .mainnet)
// From WIF (Wallet Import Format) — fails on bad checksum or
// wrong network byte
guard let key3 = PrivateKey(wif: "L1aW4...") else { return }
// Properties
let pub: PublicKey = key.publicKey // derived on-demand
let addr: Address = key.address // P2PKH for the network
let raw: Data = key.data // 32 raw bytesWIF Round-trip
Wallet Import Format is the standard textual encoding for private keys — a Base58Check string with a network-discriminating version byte:
let key = PrivateKey(network: .mainnet)
let wif: String = key.toWif() // "L1aW4...", 52 chars compressed
let restored = PrivateKey(wif: wif) // returns OptionalPublicKey
A PublicKey is the curve point a private key derives to, plus its compression flag.
// From hex (33 or 65 bytes)
guard let pk = PublicKey(hex: "02a3b1c...") else { return }
// From DER bytes (33 / 65)
guard let pk2 = PublicKey(fromDer: derBytes, isStrict: true) else {
return
}
// Serialize back
let derCompressed: Data = pk.toDer(compressed: true)
let derUncompressed: Data = pk.toDer(compressed: false)
let raw: Data = pk.toBuffer() // 33 or 65 bytes
// Address (P2PKH)
let addr: Address = pk.addressStrict vs Lax DER Parsing
The isStrict flag on init(fromDer:isStrict:) controls whether the parser rejects malformed prefixes. Production callers should always pass true; the lax mode exists only for compatibility with historical malleable encodings.
Address
Address is a P2PKH (Pay-to-Public-Key-Hash) destination. Construction is total — pass any of the upstream key types and you get an address back:
let priv = PrivateKey(network: .mainnet)
let addr1 = Address(priv) // from PrivateKey
let addr2 = Address(priv.publicKey) // from PublicKey
let addr3 = Address(bip32, network: .mainnet) // from a Bip32 node
// Parse a Base58Check string
guard let addr4 = Address(fromString: "1A1zP1...", network: .mainnet)
else { return }Output Script
To pay an address, you need its locking script:
let script: Script = address.toTxOutputScript()
// OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIGAddress produces a standard P2PKH script. For non-standard outputs (P2SH, multisig, OP_RETURN), build the Script directly.
HD Wallets — BIP-39 & BIP-32
Bip39 — Mnemonic Phrases
A 12-word or 24-word phrase from the BIP-39 word list. The phrase encodes 128 or 256 bits of entropy plus a checksum, and deterministically derives a 64-byte seed.
// Standard 24-word phrase (256 bits of entropy)
let phrase: String = Bip39.create(strength: .high)
// 12-word phrase (128 bits of entropy)
let short: String = Bip39.create(strength: .normal)
// From specific entropy (e.g. dice-rolled 32 bytes)
let custom: String = Bip39.create(entropy: myEntropy)
// Convert phrase to seed (with optional passphrase)
let seed: Data = Bip39.createSeed(
mnemonic: phrase,
withPassphrase: ""
)Bip32 — Hierarchical Deterministic Keys
A Bip32 node represents a single point in the HD key tree. From the seed produced by Bip39, you derive a master node, then descend by path:
let seed = Bip39.createSeed(mnemonic: phrase)
let master = Bip32(seed: seed, network: .mainnet)
// Derive m/44'/236'/0'/0/0 (BIP-44 BSV receive address index 0)
guard let node = master.derivedKey(path: "m/44'/236'/0'/0/0")
else { return }
let priv: PrivateKey? = node.privateKey
let pub: PublicKey = node.publicKey
let addr: Address = node.addressDerivationNode
Inside the path string, each step is encoded as a DerivationNode:
public enum DerivationNode {
case hardened(UInt32) // 0x80000000 | index
case notHardened(UInt32)
}
// Equivalent to "m/44'/236'/0'"
let n1 = DerivationNode.hardened(44)
let n2 = DerivationNode.hardened(236)
let n3 = DerivationNode.hardened(0)Hardened derivation breaks the public-key-only chain — you cannot derive hardened children from an extended public key alone. This is intentional: it isolates account-level keys from leaf-level exposure.
Public-Only Derivation
For watching-only wallets, you can hand someone an extended public key and let them derive child addresses without ever seeing the private side:
let masterPub = master.toPublic()
let xpub: String = masterPub.toString()
// On the receiving device:
guard let pubMaster = Bip32(string: xpub) else { return }
guard let leaf = pubMaster.derivedKey(path: "m/0/0")
else { return }
// leaf.privateKey == nil, leaf.publicKey is validWire Format
The Base58Check serialization (xprv..., xpub...) round-trips through toString() / init?(string:) and matches the BIP-32 specification, byte for byte. SwiftBSV-produced xprv can be loaded by any standards-compliant BIP-32 implementation.
Type42 — Invoice-Numbered Keys
Type42 generates a fresh keypair per invoice number, mixing together one party's private key, the other party's public key, and a string. Two parties who share their public keys offline can independently derive the same address for any invoice number, without ever exchanging private material.
The Three Derivation Modes
// You have your private key. Counterparty has their public key.
// Both of you can compute the same shared invoice key.
let derived: PrivateKey? = Type42.derivePrivateKey(
privateKey: myPrivateKey,
counterpartyPublicKey: theirPublicKey,
invoiceNumber: "invoice-2026-01-001"
)// Derive a child public key. Requires your own private key for
// the ECDH step (ECDH needs one private key and one public key).
let derivedPub: PublicKey? = Type42.derivePublicKey(
publicKey: theirPublicKey,
ownPrivateKey: myPrivateKey,
invoiceNumber: "invoice-2026-01-001"
)// Self-derivation — counterparty == self. Used for fresh
// receive addresses inside a single wallet without coordinating
// with anyone.
let receive: PrivateKey? = Type42.deriveSelf(
privateKey: masterKey,
invoiceNumber: "wallet/0/recv/42"
)Henceforth Path Conventions
The wallet uses Type42 invoice numbers as a flat namespace:
wallet/0/recv/N— receive addresses (wasm/44'/236'/0'/0/Nin BIP-44)wallet/0/change/N— change addressespaymail/<handle>— per-paymail keysbap/<id>— BAP identity attestation keys
The convenience wrappers deriveChangeKey, derivePaymentKey, deriveEncryptionKey, and deriveBAPIdentity build the appropriate invoice-number string and call through to derivePrivateKey.
ECDH Shared Secret
The mathematical operation underpinning Type42 — and ECIES, BRC-103 auth, and any other protocol that needs a shared session key — is ECDH:
let secret: [UInt8]? = Type42.ecdhSharedSecret(
privateKey: myPrivateKey,
publicKey: theirPublicKey
)
// 33 bytes — compressed serialization of the shared pointThe result is the compressed serialization of the curve point obtained by multiplying the counterparty's public key by your private key. Symmetric: they get the same point by multiplying your public key by their private key.
Signatures and ECIES
ECDSA Signing
ECDSA is a public final class with two main static methods. sign returns a 64-byte compact signature (Data); verifySignature accepts a DER-encoded signature and throws on structural parse failures.
let key = PrivateKey(network: .mainnet)
let messageHash = Crypto.sha256sha256(message) // 32-byte hash
// Returns 64-byte compact signature (r || s, not DER)
let compactSig: Data = ECDSA.sign(messageHash, privateKey: key.data)
// Verify a DER-encoded signature (used by OP_CHECKSIG path)
let ok: Bool = try ECDSA.verifySignature(
derSig,
message: messageHash,
publicKeyData: key.publicKey.toDer()
)For signing transactions and producing DER-encoded signatures suitable for embedding in scriptSig, use the higher-level Crypto.sign and TxBuilder.signInTx — those wrap the secp256k1 DER path. The raw ECDSA.sign is the compact-signature path used internally by the compact-signing pipeline.
Sighash Types
SighashType is a bit-or of a base mode and the FORKID flag. BSV defaults to ALL | FORKID.
SighashType.BSV.ALL // sign every input + every output
SighashType.BSV.NONE // sign every input, no outputs
SighashType.BSV.SINGLE // sign every input, only matching output
SighashType.BSV.ALL_ANYONECANPAY // ALL | ANYONECANPAYThe FORKID flag was added at the BCH/BSV chain split to prevent transaction replay between BTC and the new chains. Every BSV transaction signature includes it.
Bitcoin Signed Message (BSM)
BSM is the convention for signing arbitrary text with a Bitcoin private key, returning a Base64-encoded compact signature that any BSM-aware tool can verify against a Bitcoin address.
let priv = PrivateKey(network: .mainnet)
let addr = priv.address
let msg = "I authorize transaction abc123..."
// Returns Base64 string
let sig: String = BitcoinSignedMessage.sign(
message: msg,
privateKey: priv
)
// Anyone with the address + message + signature can verify
let ok: Bool = BitcoinSignedMessage.verify(
message: msg,
signature: sig,
address: addr
)The verifier never sees the public key directly: it's recovered from the compact signature and matched against the supplied address.
Recoverable Signatures and Compression
BSM signatures encode a 1-byte recovery header that selects which of up to four candidate public keys was the signer. Two bits encode parity and the second-derivative branch; one extra bit encodes whether the signer's public key was compressed when the address was derived. Without the compression bit, verify cannot tell whether the recovered key should be hashed in compressed or uncompressed form. SwiftBSV preserves the compression flag across the round trip.
ECIES (BIE1 Encryption)
The BIE1 Wire Format
"BIE1" || senderPubKey || ciphertext || HMAC
4 33 N 32The sender public key is 33 bytes (compressed). The HMAC is 32 bytes (HMAC-SHA-256). The ciphertext is AES-128-CBC over the plaintext, padded with PKCS#7. Total overhead is 69 bytes regardless of message length.
Key Derivation
The shared secret is the compressed encoding (33 bytes) of the ECDH point senderPrivKey * recipientPubKey. From it:
key = SHA-512(sharedPoint) // full 33-byte compressed point, not just X
iv = key[0:16] // AES-CBC IV
kE = key[16:32] // AES-CBC key (128-bit)
kM = key[32:64] // HMAC-SHA-256 keyAPI
import SwiftBSV
let sender = PrivateKey(network: .mainnet)
let recipient = PrivateKey(network: .mainnet)
let recipientPub = recipient.publicKey
// senderPrivateKey is required — BIE1 always embeds the sender's pubkey.
let plaintext: Data = "secret".data(using: .utf8)!
let cipher: Data = try ECIESEncryption.encrypt(
plaintext: plaintext,
senderPrivateKey: sender,
recipientPublicKey: recipientPub
)
// Recipient — only needs their own private key.
let recovered: Data = try ECIESEncryption.decrypt(
ciphertext: cipher,
recipientPrivateKey: recipient
)Errors
ECIESError is a small enum:
.ecdhFailed— ECDH shared-secret computation failed.encryptionFailed— AES-CBC encryption failed.decryptionFailed— AES-CBC decryption failed (typically padding error).invalidCiphertext— wrong magic prefix, truncated input, or non-canonical encoding.invalidPublicKey— the embedded sender public key didn't deserialize as a valid curve point.hmacVerificationFailed— ciphertext failed authentication
Scripts and Opcodes
Script Anatomy
A Bitcoin Script is a sequence of Chunks — each chunk is either a single opcode or a PUSHDATA containing literal bytes.
let priv = PrivateKey(network: .mainnet)
let addr = priv.address
// Build by hand — Script is a class; append/appendData throw.
let script = try Script()
.append(.OP_DUP)
.append(.OP_HASH160)
.appendData(Crypto.sha256ripemd160(priv.publicKey.toBuffer()))
.append(.OP_EQUALVERIFY)
.append(.OP_CHECKSIG)
// Or use the convenience
let sameScript = addr.toTxOutputScript()Opcode Catalogue
Opcodes live in Sources/SwiftBSV/Scripts/OP_CODE/ grouped by category:
- Push Data —
OP_FALSE/0,OP_PUSHDATA1/2/4,OP_1NEGATE,OP_1…OP_16 - Flow Control —
OP_NOP,OP_IF,OP_NOTIF,OP_ELSE,OP_ENDIF,OP_VERIFY,OP_RETURN - Stack —
OP_DUP,OP_DROP,OP_SWAP,OP_OVER,OP_ROT,OP_PICK,OP_ROLL,OP_DEPTH… - Splice —
OP_CAT,OP_SPLIT,OP_NUM2BIN,OP_BIN2NUM,OP_SIZE - Bitwise Logic —
OP_INVERT,OP_AND,OP_OR,OP_XOR,OP_EQUAL,OP_EQUALVERIFY - Arithmetic —
OP_ADD,OP_SUB,OP_MUL,OP_DIV,OP_MOD,OP_BOOLAND,OP_BOOLOR… - Crypto —
OP_HASH160,OP_HASH256,OP_SHA1,OP_SHA256,OP_RIPEMD160,OP_CHECKSIG,OP_CHECKMULTISIG - Lock Time —
OP_CHECKLOCKTIMEVERIFY,OP_CHECKSEQUENCEVERIFY - Reserved Words — the Chronicle-restored opcodes that BSV brought back from the original protocol set
The full enumeration is exhaustive — BSV restored every opcode that BTC removed at the chain split. The library exposes them all as enum cases on OpCode.
Transactions and TxBuilder
Transaction Structure
A BSV transaction is the same algebraic object the white paper describes:
Transaction
+-- version (4 bytes)
+-- inputs (varint count + N inputs)
| \-- (prevTxid, prevVout, scriptSig, sequence)
+-- outputs (varint count + M outputs)
| \-- (value, scriptPubKey)
\-- lockTime (4 bytes)SwiftBSV exposes Transaction, TransactionInput, and TransactionOutput as value types. You can construct one by hand, but the TxBuilder fluent API is the conventional path.
TxBuilder — Fluent Construction
TxBuilder is a struct. The API is split by intent:
- Configuration setters (
setVersion,setFeePerKb,setChangeAddress,setChangeScript,setNLockTime) are non-mutating and return a new copy. Use them with let-bindings and chain-and-capture. - State-mutating ops (
inputFromX,outputToX,build,signInTx) aremutating. They modifyselfin place; the binding must bevar.
let alice = PrivateKey(network: .mainnet)
let bob = Address(fromString: "1A1zP1...", network: .mainnet)!
let utxo: TransactionOutput = ... // from your UTXO source
let prevTxid: Data = ...
let prevVout: UInt32 = 0
// var because inputFromPubKeyHash/outputToAddress/build are mutating
var builder = TxBuilder()
.setVersion(2)
.setFeePerKb(50) // UInt64 satoshis per kilobyte
.setChangeAddress(alice.address)
builder.inputFromPubKeyHash(
txHashBuffer: prevTxid,
txOutNum: prevVout,
txOut: utxo,
pubKey: alice.publicKey
)
builder.outputToAddress(value: 50_000, address: bob)
// var because build and signInTx are also mutating
var built = try builder.build(useAllInputs: true)
// Sign every input
for i in 0..<built.transaction.inputs.count {
_ = built.signInTx(
nIn: i,
privateKey: alice
)
}
let raw: Data = built.transaction.serialized()
let txid: String = built.transaction.txIDMethod Reference
// Non-mutating — return a new TxBuilder copy
.setVersion(_ version: UInt32)
.setNLockTime(_ nLockTime: UInt32)
.setFeePerKb(_ fee: UInt64) // satoshis per kilobyte
.setChangeAddress(_ changeAddress: Address)
.setChangeScript(_ changeScript: Script)
.inputFromScript(
_ txHashBuffer: Data,
txOutNum: UInt32,
txOut: TransactionOutput,
script: Script,
nSequence: UInt32
)
.inputFromPubKeyHash(
txHashBuffer: Data,
txOutNum: UInt32,
txOut: TransactionOutput,
pubKey: PublicKey,
nSequence: UInt32 = 0xffffffff,
nHashType: SighashType = SighashType.BSV.ALL
)
.outputToAddress(value: UInt64, address: Address)
.outputToScript(value: UInt64, script: Script)build(useAllInputs:)
build(useAllInputs:) computes a final fee against feePerKb, picks UTXOs to cover the outputs, and emits a change output if there's leftover value. Pass true to force every added input into the final transaction; pass false to let the builder pick the minimum subset.
signInTx(nIn:privateKey:)
Signs a single input with the supplied private key. Repeat for every input. The signature uses SighashType.BSV.ALL | FORKID by default.
Parsing a Transaction
Transaction.deserialize(_:) is the inverse of serialized() — it reconstructs the value type from wire bytes. Those bytes are almost never yours: they arrive from a peer, a block, a pasted hex string, a QR code, a dApp handing you a transaction to inspect. So deserialize is a throwing function, and failure is part of its contract.
let raw: Data = ... // from the network, a file, a paste — untrusted
do {
let tx = try Transaction.deserialize(raw)
// tx.inputs, tx.outputs, tx.txID — safe to read
} catch {
// DeserializationError — truncated or structurally invalid; reject it
}Every read inside the parser is bounds-checked before it runs. A buffer that ends in the middle of a script, an input count that claims more inputs than the remaining bytes can hold, a length prefix that overflows Int — each throws DeserializationError rather than reading past the end:
.unexpectedEndOfStream— a read ran off the end of the buffer. The input is truncated..malformedData— a length or count field declares a value too large to represent. The input is structurally invalid, not merely short.
BEEF and BUMP Parsing (BRC-62 / BRC-74)
SwiftBSV ships a complete BEEF/BUMP parser. BEEF bundles transactions with their Merkle proofs for offline SPV verification. BUMP is the compact binary Merkle path format those proofs use.
// From hex string
let beef: BEEF = try BEEFParser.parse(hex: hexString)
// From raw bytes
let beef2: BEEF = try BEEFParser.parse(data: rawData)
// Access the parsed structure
let bumps: [BUMP] = beef.bumps
let txs: [BEEFTransaction] = beef.transactions
let hasBUMP: Bool = txs.first?.hasBUMP ?? false
let bumpIndex: UInt64? = txs.first?.bumpIndexBEEFParser.ParseError cases: .invalidVersion(UInt32), .invalidData, .unexpectedEnd.
let bump: BUMP = beef.bumps[0]
// bump.blockHeight, bump.treeHeight, bump.levels: [BUMPLevel]
for level in bump.levels {
for leaf in level.leaves {
// leaf.offset, leaf.hash, leaf.txid, leaf.duplicate
}
}
// Extract transaction IDs referenced in this BUMP
let txids: [String] = bump.txidsAll BEEF and BUMP types (BEEF, BEEFTransaction, BUMP, BUMPLevel, BUMPLeaf) are Sendable structs — safe to pass across actor boundaries.
Simplified Payment Verification
BlockHeader Protocol
Anything that can answer the eight standard fields conforms to the BlockHeader protocol. You don't have to use SwiftBSV's own model type — WhatsOnChain's WOCGetHeadersModel (in Henceforth) conforms directly:
public protocol BlockHeader {
var hash: String { get }
var height: Int { get }
var version: Int { get }
var merkleroot: String { get }
var time: Date { get }
var nonce: Int { get }
var bits: String { get }
var previousblockhash: String { get }
}BlockHeaderValidator
The validator runs three checks:
let validator = BlockHeaderValidator()
// 1. Single-header proof-of-work
let powOK: Bool = validator.validateProofOfWork(header: header)
// 2. Chain-link to parent
let linkOK: Bool = validator.validateChainLink(
header: child,
previousHeader: parent
)
// 3. Full chain — sorts by height, validates PoW + linking
let result: ChainValidationResult =
validator.validateHeaderChain(headers: headers)
if result.isValid {
print("validated \(result.validatedCount) headers")
} else {
print(result.errorMessage ?? "unknown error")
}Block Hashing
// Serialize the 80-byte header
let serialized: Data? = validator.serializeBlockHeader(header: h)
// Double-SHA-256 and reverse to display byte order
let hash: String? = validator.calculateBlockHash(header: h)Target from Bits
The bits field encodes the difficulty target in compact form:
let target: UInt256? = validator.targetFromBits(bits: header.bits)
// PoW passes if computed hash, interpreted as UInt256,
// is strictly less than target
let hashValue: UInt256? = UInt256(hexString: computedHash)
let powOK = hashValue! < target!UInt256 is a custom big-endian 256-bit comparable struct.
Merkle Proof Formats
SwiftBSV understands three Merkle proof formats:
- Standard
MerkleProof— generic representation:(txid, blockHash, index, nodes). - TSC (BRC-74)
TSCMerkleProof— the Technical Standards Committee compact format used by BSV miners and indexers. - WhatsOnChain
WOCMerkleProof— the WoC API response shape with directional branches ("L"/"R").
let proof = MerkleProof(
txid: "...",
blockHash: "...",
blockHeight: 800_000,
merkleRoot: "...",
index: 42,
nodes: ["aaaa...", "bbbb...", ...]
)
let verifier = MerkleVerifier()
let ok = verifier.verifyMerkleProof(
proof,
expectedMerkleRoot: header.merkleroot
)let tsc = TSCMerkleProof(
index: 42,
txOrId: "...",
target: header.hash, // block hash, not merkle root
nodes: ["...", "..."],
targetType: "merkleroot",
proofType: "branch",
composite: false
)
let okTSC = verifier.verifyTSCProof(
tsc,
expectedMerkleRoot: header.merkleroot
)// Verify the WoC proof against a header's merkle root that you
// fetched independently. NEVER trust the proof's embedded root —
// always cross-check with a header from the chain-validated store.
let okWOC = verifier.verifyWOCProofAgainstRoot(
wocProof,
expectedMerkleRoot: localHeader.merkleroot
)Pair Hashing in Display vs Wire Order
The trickiest part of Merkle verification is the byte-order convention. Bitcoin hashes appear in the API and the UI in display order (most significant byte first), but the underlying double-SHA-256 operates on wire order (least significant byte first). doubleSHA256HashPair encapsulates this:
public func doubleSHA256HashPair(left: String, right: String) -> String {
let leftBytes = Data(hex: left)
let rightBytes = Data(hex: right)
var combined = Data(leftBytes.reversed())
combined.append(Data(rightBytes.reversed()))
let hash1 = Data(combined).sha256()
let hash2 = Data(hash1).sha256()
return Data(Data(hash2).reversed()).hex
}The two reversals — on input and on output — mean the function takes display-hex in and produces display-hex out, while internally hashing in wire order.
SPV Errors
SPVError is the closed enum returned by verification failures:
.invalidMerkleProof— the proof structure is malformed.blockHeaderNotFound— the consumer was asked to verify against a header it doesn't have.merkleRootMismatch— the proof verified to a different root than the header.invalidProofFormat— format-level rejection.networkError(String)— carried up from the consumer's network layer.txNotInBlock— proof unavailable for the transaction (typical when a tx is mempool-only).invalidBlockHash— the block hash failed PoW or didn't link.headerChainNotSynced(requiredHeight: Int, localTip: Int)— the tx is in a block above the locally validated tip; the consumer should retry once sync catches up, not present this as a verification failure
The distinction between .blockHeaderNotFound and .headerChainNotSynced is what lets the consumer show "⏳ SPV PENDING" for sync gaps and "✗ SPV FAILED" for genuine verification failures.
Integration
SwiftPM
In your Package.swift:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16)],
dependencies: [
.package(
url: "https://github.com/henryhudson/SwiftBSV",
branch: "main"
)
],
targets: [
.target(
name: "MyApp",
dependencies: ["SwiftBSV"]
)
]
)Xcode
File → Add Packages… → paste the GitHub URL → Branch: main. Add SwiftBSV to your application target's framework list.
Platform Requirements
- Swift tools 5.9+ (the
Package.swiftdeclares this minimum) - iOS 16 / macOS 13 / tvOS 16 or later
- Xcode 16+ for full Swift 6 strict-concurrency builds
Importing
import SwiftBSV
// Everything documented in this book is in the SwiftBSV module.
// No subspec or qualified import — single import covers all
// public surface.Swift 6 Concurrency Notes
Sendable types you can pass freely across actor boundaries: PrivateKey, PublicKey, Address, Bip32, Network, SighashType, BInt, all SPV models (MerkleProof, ChainValidationResult, TSCMerkleProof, WOCMerkleProof), BEEF, BUMP, BEEFTransaction, BUMPLevel, BUMPLeaf.
A small number of types remain non-Sendable because mutation is part of their interface:
TxBuilder— holds an evolvingTransaction; mutate it on one isolation domain and pass the resultingTransaction.serialized()Dataacross boundaries.Transaction,TransactionInput,TransactionOutput— mutable types; notSendable.Script— aclasswith mutable internal state; notSendable.Bip39— a static-only entry point in practice; construction is a no-op.
License
SwiftBSV is published under the same license as the upstream Yenom BitcoinKit it forked from — see LICENSE in the repository root.
Contributing
The repository accepts pull requests for:
- Bug fixes against documented behavior
- New tests — particularly in the SPV, ECIES, and Type42 areas where coverage is shallower than ECDSA
- Documentation corrections — this book is generated from
swiftbsv.texin the LaTeX repo - Performance improvements with benchmarks
API additions are accepted on a case-by-case basis — the goal is a small library, not a kitchen sink. Open an issue describing the use case before opening a PR for a new public type.