Last active
May 2, 2020 17:50
-
-
Save yvetterowe/de42b30a7ef5cdc6e5c5a0312302c3f3 to your computer and use it in GitHub Desktop.
Toy BTC transaction verification
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - Primitives | |
typealias Address = String | |
typealias Signature = String | |
typealias Coins = String | |
// MARK: - Transaction | |
enum Transaction { | |
case coinbase(id: ID, outputs: [Output]) | |
case standard(id: ID, inputs: [Input], outputs: [Output]) | |
typealias ID = String | |
struct Input { | |
let previousTxID: ID | |
let previousTxOutputIndex: Int | |
let signature: Signature | |
} | |
struct Output { | |
let recipientAddress: Address | |
let value: Coins | |
} | |
struct UnspentTransactionOutputID: Hashable { | |
let txID: ID | |
let txOutputIndex: Int | |
} | |
} | |
extension Transaction { | |
var inputs: [Input] { | |
switch self { | |
case .coinbase: return [] | |
case let .standard(_,inputs,_): return inputs | |
} | |
} | |
var outputs: [Output] { | |
switch self { | |
case let .coinbase(_,outputs): return outputs | |
case let .standard(_,_,outputs): return outputs | |
} | |
} | |
var id: ID { | |
switch self { | |
case let .coinbase(id,_): return id | |
case let .standard(id,_,_): return id | |
} | |
} | |
} | |
extension Transaction.Input { | |
var uxtoID: Transaction.UnspentTransactionOutputID { | |
return .init( | |
txID: previousTxID, | |
txOutputIndex: previousTxOutputIndex | |
) | |
} | |
} | |
// MARK: - CustomStringConvertible | |
extension Transaction.Output: CustomStringConvertible { | |
var description: String { | |
return "owner: \(recipientAddress.prefix { $0 != "๐ฎ" }) | amount: \(value)" | |
} | |
} | |
extension Transaction.UnspentTransactionOutputID: CustomStringConvertible { | |
var description: String { | |
return "\(txID)(\(txOutputIndex))" | |
} | |
} | |
extension Node.UnspentTransactionOutputPool: CustomStringConvertible { | |
var description: String { | |
return self.map {"\($0.key) | \($0.value) \n"}.reduce("", +) | |
} | |
} | |
extension Node.TransactionValidationError: CustomStringConvertible { | |
var description: String { | |
switch self { | |
case .inputNotFromUTXOPool: | |
return "Input not found in UTXO pool" | |
case .inputInvalidSignature: | |
return "Invalid input signature" | |
case .inputClaimMultipleSameUTXO: | |
return "Same UTXO claimed multiple times in inputs" | |
case let .totalInputLessThanTotalOutput(inputValue, outputValue): | |
return "Total input \(inputValue) is less than total output \(outputValue)" | |
} | |
} | |
} | |
// MARK: - Node | |
final class Node { | |
typealias UnspentTransactionOutputPool = [Transaction.UnspentTransactionOutputID: Transaction.Output] | |
enum TransactionValidationError: Error { | |
case inputNotFromUTXOPool | |
case inputInvalidSignature | |
case inputClaimMultipleSameUTXO | |
case totalInputLessThanTotalOutput(inputValue: Coins, outputValue: Coins) | |
} | |
private var utxoPool: UnspentTransactionOutputPool = [:] | |
init() {} | |
func addTransaction(_ transaction: Transaction) { | |
print("Try adding a transaction...") | |
switch isValidTransaction(transaction) { | |
case .success: | |
for (index, output) in transaction.outputs.enumerated() { | |
let uxtoID = Transaction.UnspentTransactionOutputID( | |
txID: transaction.id, | |
txOutputIndex: index) | |
utxoPool[uxtoID] = output | |
} | |
for input in transaction.inputs { | |
utxoPool.removeValue(forKey: input.uxtoID) | |
} | |
print("๐ค hooray your transaction is validated!") | |
case let .failure(error): | |
print("๐ uh oh your transaction is denied. \nReason: \(error)") | |
} | |
print("Current UTXO pool:\n\(utxoPool.description)") | |
} | |
private func isValidTransaction(_ transaction: Transaction) -> Result<Void, TransactionValidationError> { | |
switch transaction{ | |
case .coinbase: | |
return .success(()) | |
case let .standard(_, inputs, outputs): | |
var claimedUTXOIDs: Set<Transaction.UnspentTransactionOutputID> = .init() | |
var totalInputValue: Coins = "" | |
for input in inputs { | |
// Input must be in current UTXO pool | |
guard let utxoToClaim = utxoPool[input.uxtoID] else { | |
return .failure(.inputNotFromUTXOPool) | |
} | |
// Same UTXO can't be claimed multiple times | |
if claimedUTXOIDs.contains(input.uxtoID) { | |
return .failure(.inputClaimMultipleSameUTXO) | |
} | |
// Input signature must be valid | |
let isSignatureValid = verify( | |
signature: input.signature, | |
address: utxoToClaim.recipientAddress, | |
coins: utxoToClaim.value) | |
guard isSignatureValid else { | |
return .failure(.inputInvalidSignature) | |
} | |
claimedUTXOIDs.insert(input.uxtoID) | |
totalInputValue += utxoToClaim.value | |
} | |
// Total output can't be more than total input | |
let totalOutputValue = outputs.map {$0.value}.reduce("", +) | |
if totalInputValue >= totalOutputValue { | |
return .success(()) | |
} else { | |
return .failure( | |
.totalInputLessThanTotalOutput(inputValue: totalInputValue, outputValue: totalOutputValue) | |
) | |
} | |
} | |
} | |
private func verify(signature: Signature, address: Address, coins: Coins) -> Bool { | |
let expectedRecipient = address.prefix { $0 != "๐ฎ" } | |
let signer = String(signature.split(separator: "๐").first!) | |
return expectedRecipient == signer | |
} | |
} | |
// MARK: - Profit! | |
let node = Node() | |
/** | |
Case 1 - ๐ฉ๐ปโ๐พ and ๐ฆน๐ปโโ๏ธ both get initial ๐ฐ๐ฐ๐ฐ from a coinbase transaction | |
*/ | |
let initialCoins: Transaction = .coinbase( | |
id: "๐ฎ", | |
outputs: [ | |
.init(recipientAddress: "๐ฉ๐ปโ๐พ๐ฎ", value: "๐ฐ๐ฐ๐ฐ"), | |
.init(recipientAddress: "๐ฆน๐ปโโ๏ธ๐ฎ", value: "๐ฐ๐ฐ๐ฐ"), | |
] | |
) | |
node.addTransaction(initialCoins) | |
/** | |
Case 2 - ๐ฉ๐ปโ๐พ pays ๐ฐ๐ฐ to ๐ฆน๐ปโโ๏ธ | |
*/ | |
let realAlicePayBob: Transaction = .standard( | |
id: "๐น", | |
inputs: [ | |
.init( | |
previousTxID: "๐ฎ", | |
previousTxOutputIndex: 0, | |
signature: "๐ฉ๐ปโ๐พ๐๐น" | |
), | |
], | |
outputs: [ | |
.init(recipientAddress: "๐ฆน๐ปโโ๏ธ๐ฎ", value: "๐ฐ๐ฐ"), | |
.init(recipientAddress: "๐ฉ๐ปโ๐พ๐ฎ", value: "๐ฐ"), // change | |
] | |
) | |
node.addTransaction(realAlicePayBob) | |
/** | |
Case 3 - ๐ฆน๐ปโโ๏ธ counterfeits ๐ฐ๐ฐ๐ฐ out of thin air | |
*/ | |
let bobDayDream: Transaction = .standard( | |
id: "๐ธ", | |
inputs: [ | |
.init( | |
previousTxID: "๐ฅ", | |
previousTxOutputIndex: 1, | |
signature: "๐ฆน๐ปโโ๏ธ๐๐ธ" | |
), | |
], | |
outputs: [ | |
.init(recipientAddress: "๐ฆน๐ปโโ๏ธ๐ฎ", value: "๐ฐ๐ฐ๐ฐ"), | |
] | |
) | |
node.addTransaction(bobDayDream) | |
/** | |
Case 4 - ๐ฆน๐ปโโ๏ธ counterfeits ๐ฐ paid by ๐ฉ๐ปโ๐พ | |
*/ | |
let fakeAlicePayBob: Transaction = .standard( | |
id: "๐บ", | |
inputs: [ | |
.init( | |
previousTxID: "๐น", | |
previousTxOutputIndex: 1, | |
signature: "๐ฆน๐ปโโ๏ธ๐๐บ" | |
), | |
], | |
outputs: [ | |
.init(recipientAddress: "๐ฆน๐ปโโ๏ธ๐ฎ", value: "๐ฐ"), | |
] | |
) | |
node.addTransaction(fakeAlicePayBob) | |
/** | |
Case 5 - ๐ฉ๐ปโ๐พ tries paying ๐ฐ๐ฐ to ๐ฆน๐ปโโ๏ธ by redeeming ๐ฐ twice | |
*/ | |
let alicePayBobDoubleSpend: Transaction = .standard( | |
id: "๐ฅ", | |
inputs: [ | |
.init( | |
previousTxID: "๐น", | |
previousTxOutputIndex: 1, | |
signature: "๐ฉ๐ปโ๐พ๐๐ฅ" | |
), | |
.init( | |
previousTxID: "๐น", | |
previousTxOutputIndex: 1, | |
signature: "๐ฉ๐ปโ๐พ๐๐ฅ" | |
), | |
], | |
outputs: [ | |
.init(recipientAddress: "๐ฆน๐ปโโ๏ธ๐ฎ", value: "๐ฐ๐ฐ"), | |
] | |
) | |
node.addTransaction(alicePayBobDoubleSpend) | |
/** | |
Case 6 - ๐ฆน๐ปโโ๏ธ tries aggregating ๐ฐ๐ฐ๐ฐ + ๐ฐ๐ฐ into ๐ฐ๐ฐ๐ฐ๐ฐ๐ฐ๐ฐ | |
*/ | |
let bobAggregateChangesMoreThanHeOwn: Transaction = .standard( | |
id: "๐ป", | |
inputs: [ | |
.init( | |
previousTxID: "๐ฎ", | |
previousTxOutputIndex: 1, | |
signature: "๐ฆน๐ปโโ๏ธ๐๐ป" | |
), | |
.init( | |
previousTxID: "๐น", | |
previousTxOutputIndex: 0, | |
signature: "๐ฆน๐ปโโ๏ธ๐๐ป" | |
), | |
], | |
outputs: [ | |
.init(recipientAddress: "๐ฆน๐ปโโ๏ธ๐ฎ", value: "๐ฐ๐ฐ๐ฐ๐ฐ๐ฐ๐ฐ"), | |
] | |
) | |
node.addTransaction(bobAggregateChangesMoreThanHeOwn) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment