Skip to content

Commit e8b5476

Browse files
committed
add bitcoin block and transaction serialization helpers
1 parent d946dd0 commit e8b5476

File tree

5 files changed

+487
-0
lines changed

5 files changed

+487
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// BTCBlock.swift
3+
// DirectBindingsApp
4+
//
5+
// Created by Arik Sosman on 3/28/22.
6+
//
7+
8+
import Foundation
9+
10+
/// serialzation:
11+
/// 4 bytes: 0xD9B4BEF9
12+
/// 4 bytes: block size (remaining byte count)
13+
/// 80 bytes: block header
14+
/// VarInt (1-9 bytes): transaction count
15+
/// transactions
16+
17+
class BTCBlock: Equatable {
18+
19+
var version: UInt32 = 2
20+
var previousBlockHash: [UInt8] = []
21+
var merkleRoot: [UInt8]?
22+
var timestamp: UInt32 = 0
23+
var difficultyTarget: UInt32 = 0
24+
var nonce: UInt32 = 0
25+
26+
// each transaction is a uint8 array
27+
var transactions: [BTCTransaction] = []
28+
29+
func calculateMerkleRoot() -> [UInt8] {
30+
// TODO: merkleize the transactions
31+
if let merkleRoot = self.merkleRoot {
32+
return merkleRoot
33+
}
34+
return [UInt8](repeating: 0, count: 32)
35+
}
36+
37+
func calculateHeader() -> [UInt8] {
38+
var header = [UInt8]()
39+
40+
do {
41+
let versionBytes = withUnsafeBytes(of: self.version.littleEndian) { bytes in
42+
Array(bytes)
43+
}
44+
assert(versionBytes.count == 4)
45+
header.append(contentsOf: versionBytes)
46+
}
47+
48+
do {
49+
assert(self.previousBlockHash.count == 32)
50+
header.append(contentsOf: self.previousBlockHash)
51+
}
52+
53+
do {
54+
let merkleRoot = self.calculateMerkleRoot()
55+
assert(merkleRoot.count == 32)
56+
header.append(contentsOf: merkleRoot)
57+
}
58+
59+
do {
60+
let timestampBytes = withUnsafeBytes(of: self.timestamp.littleEndian) { bytes in
61+
Array(bytes)
62+
}
63+
assert(timestampBytes.count == 4)
64+
header.append(contentsOf: timestampBytes)
65+
}
66+
67+
do{
68+
let difficultyTargetBytes = withUnsafeBytes(of: self.difficultyTarget.littleEndian) { bytes in
69+
Array(bytes)
70+
}
71+
assert(difficultyTargetBytes.count == 4)
72+
header.append(contentsOf: difficultyTargetBytes)
73+
}
74+
75+
do {
76+
let nonceBytes = withUnsafeBytes(of: self.nonce.littleEndian) { bytes in
77+
Array(bytes)
78+
}
79+
assert(nonceBytes.count == 4)
80+
header.append(contentsOf: nonceBytes)
81+
}
82+
83+
assert(header.count == 80)
84+
return header
85+
}
86+
87+
/// The double sha256 hash of the block header (note: not the entire block)
88+
/// In a well-mined block, the zeroes should be trailing, as this hash is little-endian-equivalent
89+
func calculateHash() -> [UInt8] {
90+
let blockHeader = self.calculateHeader()
91+
return BTCHashing.doubleSha256(blockHeader)
92+
}
93+
94+
func serialize() -> [UInt8] {
95+
96+
var block = [UInt8]()
97+
98+
// apparently, this only needs to be appended when it's actually part of a block
99+
/*
100+
do {
101+
let magicNumber: UInt32 = 4190024921
102+
let magicNumberBytes = withUnsafeBytes(of: magicNumber.littleEndian) { bytes in
103+
Array(bytes)
104+
}
105+
assert(magicNumberBytes.count == 4)
106+
assert(magicNumberBytes == [0xf9, 0xbe, 0xb4, 0xd9])
107+
108+
block.append(contentsOf: magicNumberBytes)
109+
}
110+
*/
111+
112+
let blockHeader = self.calculateHeader()
113+
let transactionCounter = BTCVarInt(UInt64(self.transactions.count)).serialize()
114+
let transactionData = self.transactions.reduce(into: [UInt8]()) { partialResult, currentValue in
115+
return partialResult.append(contentsOf: currentValue.serialize())
116+
}
117+
118+
/*
119+
do {
120+
let blockSize = blockHeader.count + transactionCounter.count + transactionData.count
121+
let blockSizeBytes = withUnsafeBytes(of: UInt32(blockSize).littleEndian) { bytes in
122+
Array(bytes)
123+
}
124+
assert(blockSizeBytes.count == 4)
125+
block.append(contentsOf: blockSizeBytes)
126+
}
127+
*/
128+
129+
block.append(contentsOf: blockHeader)
130+
block.append(contentsOf: transactionCounter)
131+
block.append(contentsOf: transactionData)
132+
133+
return block
134+
}
135+
136+
static func == (lhs: BTCBlock, rhs: BTCBlock) -> Bool {
137+
return lhs.calculateHash() == rhs.calculateHash()
138+
}
139+
140+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// BTCHashing.swift
3+
// DirectBindingsAppTests
4+
//
5+
// Created by Arik Sosman on 3/29/22.
6+
//
7+
8+
import Foundation
9+
import CommonCrypto
10+
11+
class BTCHashing {
12+
13+
public static let SHA_ZERO_HASH = [UInt8](repeating: 0, count: 32)
14+
public static let RIPEMD_ZERO_HASH = [UInt8](repeating: 0, count: 20)
15+
16+
private static func sha256(_ input: [UInt8]) -> [UInt8] {
17+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
18+
input.withUnsafeBytes {
19+
_ = CC_SHA256($0.baseAddress, CC_LONG(input.count), &hash)
20+
}
21+
assert(hash.count == 32)
22+
return hash
23+
}
24+
25+
static func doubleSha256(_ input: [UInt8]) -> [UInt8] {
26+
return sha256(sha256(input))
27+
}
28+
29+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//
2+
// BTCTransaction.swift
3+
// DirectBindingsApp
4+
//
5+
// Created by Arik Sosman on 3/29/22.
6+
//
7+
8+
import Foundation
9+
10+
class BTCTransaction: Equatable {
11+
12+
var version: UInt32 = 1
13+
var flag: UInt16? // let's not support witness data quite yet
14+
var inputs: [Input] = []
15+
var outputs: [Output] = []
16+
var lockTime: UInt32 = 0
17+
18+
public func serialize() -> [UInt8] {
19+
var transaction = [UInt8]()
20+
21+
var witnessData = [UInt8]()
22+
23+
do {
24+
let versionBytes = withUnsafeBytes(of: self.version.littleEndian) { bytes in
25+
Array(bytes)
26+
}
27+
assert(versionBytes.count == 4)
28+
transaction.append(contentsOf: versionBytes)
29+
}
30+
31+
if let flag = self.flag {
32+
let flagBytes = withUnsafeBytes(of: flag.littleEndian) { bytes in
33+
Array(bytes)
34+
}
35+
assert(flagBytes.count == 2)
36+
transaction.append(contentsOf: flagBytes)
37+
}
38+
39+
do {
40+
let inputCounter = BTCVarInt(UInt64(self.inputs.count)).serialize()
41+
transaction.append(contentsOf: inputCounter)
42+
43+
self.inputs.map { currentInput in
44+
transaction.append(contentsOf: currentInput.serialize())
45+
if let witness = currentInput.witness {
46+
witnessData.append(contentsOf: witness.serialize())
47+
} else if let _ = self.flag {
48+
witnessData.append(0) // empty witness for this particular input
49+
}
50+
}
51+
}
52+
53+
do {
54+
let outputCounter = BTCVarInt(UInt64(self.outputs.count)).serialize()
55+
transaction.append(contentsOf: outputCounter)
56+
57+
self.outputs.map { currentOutput in
58+
transaction.append(contentsOf: currentOutput.serialize())
59+
}
60+
}
61+
62+
transaction.append(contentsOf: witnessData)
63+
64+
do {
65+
let lockTimeBytes = withUnsafeBytes(of: self.lockTime.littleEndian) { bytes in
66+
Array(bytes)
67+
}
68+
assert(lockTimeBytes.count == 4)
69+
transaction.append(contentsOf: lockTimeBytes)
70+
}
71+
72+
return transaction
73+
}
74+
75+
public func setWitnessForInput(inputIndex: Int, witness: Witness) {
76+
self.flag = 256
77+
self.inputs[inputIndex].witness = witness
78+
}
79+
80+
public func calculateHash() -> [UInt8] {
81+
let transactionData = self.serialize()
82+
return BTCHashing.doubleSha256(transactionData)
83+
}
84+
85+
static func == (lhs: BTCTransaction, rhs: BTCTransaction) -> Bool {
86+
return lhs.calculateHash() == rhs.calculateHash()
87+
}
88+
89+
public class Input {
90+
var previousTransactionHash: [UInt8]
91+
var previousOutputIndex: UInt32
92+
var script: [UInt8]
93+
var sequence: UInt32 = 0xffffffff
94+
fileprivate var witness: Witness?
95+
96+
init(previousTransactionHash: [UInt8], previousOutputIndex: UInt32, script: [UInt8]) {
97+
self.previousTransactionHash = previousTransactionHash
98+
self.previousOutputIndex = previousOutputIndex
99+
self.script = script
100+
}
101+
102+
fileprivate func serialize() -> [UInt8] {
103+
var input = [UInt8]()
104+
105+
input.append(contentsOf: previousTransactionHash)
106+
107+
do {
108+
let outputIndexBytes = withUnsafeBytes(of: self.previousOutputIndex.littleEndian) { bytes in
109+
Array(bytes)
110+
}
111+
assert(outputIndexBytes.count == 4)
112+
input.append(contentsOf: outputIndexBytes)
113+
}
114+
115+
do {
116+
let scriptLengthCounter = BTCVarInt(UInt64(self.script.count)).serialize()
117+
input.append(contentsOf: scriptLengthCounter)
118+
input.append(contentsOf: self.script)
119+
}
120+
121+
do {
122+
let sequenceBytes = withUnsafeBytes(of: self.sequence.littleEndian) { bytes in
123+
Array(bytes)
124+
}
125+
assert(sequenceBytes.count == 4)
126+
input.append(contentsOf: sequenceBytes)
127+
}
128+
129+
return input
130+
}
131+
}
132+
133+
public struct Witness {
134+
var stackElements: [[UInt8]]
135+
136+
fileprivate func serialize() -> [UInt8] {
137+
var witness = [UInt8]()
138+
139+
let stackSizeCounter = BTCVarInt(UInt64(self.stackElements.count)).serialize()
140+
witness.append(contentsOf: stackSizeCounter)
141+
142+
self.stackElements.map { currentElement in
143+
let elementSizeCounter = BTCVarInt(UInt64(currentElement.count)).serialize()
144+
witness.append(contentsOf: elementSizeCounter)
145+
witness.append(contentsOf: currentElement)
146+
}
147+
148+
return witness
149+
}
150+
151+
}
152+
153+
public struct Output {
154+
var value: UInt64
155+
var script: [UInt8]
156+
157+
fileprivate func serialize() -> [UInt8] {
158+
var output = [UInt8]()
159+
160+
do {
161+
let valueBytes = withUnsafeBytes(of: self.value.littleEndian) { bytes in
162+
Array(bytes)
163+
}
164+
assert(valueBytes.count == 8)
165+
output.append(contentsOf: valueBytes)
166+
}
167+
168+
do {
169+
let scriptLengthCounter = BTCVarInt(UInt64(self.script.count)).serialize()
170+
output.append(contentsOf: scriptLengthCounter)
171+
output.append(contentsOf: self.script)
172+
}
173+
174+
return output
175+
}
176+
}
177+
178+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// BTCVarInt.swift
3+
// DirectBindingsApp
4+
//
5+
// Created by Arik Sosman on 3/29/22.
6+
//
7+
8+
import Foundation
9+
10+
public class BTCVarInt {
11+
12+
private let value: UInt64
13+
14+
public init(_ value: UInt64) {
15+
self.value = value
16+
}
17+
18+
public func serialize() -> [UInt8] {
19+
var varInt = [UInt8]()
20+
var valueBytes: [UInt8]
21+
22+
if self.value < 0xfd {
23+
return [UInt8(self.value)]
24+
} else if self.value <= 0xffff {
25+
varInt.append(0xfd)
26+
valueBytes = withUnsafeBytes(of: UInt16(self.value).littleEndian) { bytes in
27+
Array(bytes)
28+
}
29+
} else if self.value <= 0xffffffff {
30+
varInt.append(0xfe)
31+
valueBytes = withUnsafeBytes(of: UInt32(self.value).littleEndian) { bytes in
32+
Array(bytes)
33+
}
34+
} else {
35+
varInt.append(0xff)
36+
valueBytes = withUnsafeBytes(of: self.value.littleEndian) { bytes in
37+
Array(bytes)
38+
}
39+
}
40+
varInt.append(contentsOf: valueBytes)
41+
return varInt
42+
}
43+
44+
}

0 commit comments

Comments
 (0)