Skip to content

Instantly share code, notes, and snippets.

@yvetterowe
Last active May 2, 2020 17:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yvetterowe/de42b30a7ef5cdc6e5c5a0312302c3f3 to your computer and use it in GitHub Desktop.
Save yvetterowe/de42b30a7ef5cdc6e5c5a0312302c3f3 to your computer and use it in GitHub Desktop.
Toy BTC transaction verification
// 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