Validate Bitcoin Transaction

April 25, 2020 -
#tech #cryptocurrency

The implementation and test cases mentioned in this post can be found here.

The Trust Issue

When talking about accepting digital money, people usually have some basic concerns:

  1. Can I trust that the money is authentic and not counterfeit ?
  2. Can I trust that the money can only be spent once ?
  3. Can I trust that no one else can claim this money belongs to them and not me ?

Paper money address these concerns by its physical presence nature and advanced print technologies. For fiat money that is stored and transmitted digitally (eg. credit card, P2P payment apps, etc.), these issues are usually resolved by introducing central authorities that have a global ledger of the currency in circulation.

Same requirements apply to cryptocurrency like Bitcoin as well. If it fails either of them, it loses the trust. In this post, Iโ€™m building a simplified version (yes, no fancy Merkle tree, Script, Proof-of-Work, etc.) of Bitcoin transaction verification system for a single node - with the end goal to fool it, and lose trust on it.

What does a Bitcoin transaction look like ?

The Bitcoin system works like a double-entry bookkeeping ledger, in which all the information about a money movement is encoded in a Bitcoin transaction . Most standard P2P transactions consist of inputs and outputs, where the former describes where the fund comes from and the latter describes where itโ€™ll end up at. Coinbase transaction is an exception that doesnโ€™t have any inputs because instead of transferred from a previous owner, itโ€™s created by miners. Also, each transaction can be identified with a unique Transaction Identifier (TXID), which is the hash of raw transaction data.

enum Transaction {
    case coinbase(id: ID, outputs: [Output])
    case standard(id: ID, inputs: [Input], outputs: [Output])
}

Output gives instructions on transferring the Bitcoin: to whom and how much.

struct Output {
    let recipientAddress: Address
    let value: Coins
}

Remember that in order for the double-entry ledger to neither lose money nor create money out of thin air, an Input of a transaction must refer to the Output of a previous transaction.

Hereโ€™s how this connection is bridged. Whenever an Output is generated, itโ€™s โ€œdumpedโ€ into a pool where it waits to be redeemd by the new owner. It maintains a temporary identity as Unspent Transaction Output (UTXO) during this grace period.

struct UnspentTransactionOutputID: Hashable {
    let txID: ID
    let txOutputIndex: Int
}

let utxoPool = [Transaction.UnspentTransactionOutputID: Transaction.Output]()

When the new owner is ready to redeem the money payable to her, she starts assembling her Input that could be used to locate Output in the pool.

struct Input {
    let previousTxID: ID
    let previousTxOutputIndex: Int
    let signature: Signature
}
let utxoID = UnspentTransactionOutputID(txID: input.previousTxID, txOutputIndex: input.previousTxOutputIndex)
let utxo = utxoPool[utxoID]

But remember that this located utxo can be spotted by everyone, not limited to the recipient. Then does that mean everyone can just pick it up from the pool and claim it as theirs? Good luck with that.

Signature comes to the rescue. When an Output is dumped to the pool with recipientAddress, it follows such a protocol

Iโ€™m payable to whomever can present a signature from the key corresponding to this public address

Say if Output.recipientAddress points to Alice, Bob can never claim it either with his own signature, or a fake Aliceโ€™s signature.

How does a node verify Bitcoin transactions ?

With discussions above and the concerns thrown out at the begining, we could derive some basic principles to validate a transacion:

  1. Input must be in current UTXO pool.
  2. Input signature must be valid.
  3. Same UTXO canโ€™t be redeemd multiple times.
  4. Total input value must not be less than total output value.

Based on these principles, we can try building that simplified version of Bitcoin transaction verification system.

Letโ€™s break things.

Things only go well when they go well. Assume Alice ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ and Bob ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ are usually good citizens, but sometimes they can be bad actors as well.

Case 1. ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ and ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ both get initial ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ from a coinbase transaction

let initialCoins: Transaction = .coinbase(
    id: "๐Ÿ”ฎ",
    outputs: [
        .init(recipientAddress: "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ๐Ÿ“ฎ", value: "๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ"),
        .init(recipientAddress: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ“ฎ", value: "๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ"),
    ]
)

Everything just works! Now each of them owns some money.

๐Ÿค  hooray your transaction is validated!
Current UTXO pool:
๐Ÿ”ฎ(1) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ 
๐Ÿ”ฎ(0) | owner: ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ 

Case 2. ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ pays ๐Ÿ’ฐ๐Ÿ’ฐ to ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ

She locates the (only) previous transaction payable to her (identified as โ€œ๐Ÿ”ฎโ€), redeems it by verifying her signature - so far she has proved ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ is hers, and she can do whatever she wants with them. She sends ๐Ÿ’ฐ๐Ÿ’ฐ to ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ and sends the remaining ๐Ÿ’ฐ back to herself as change.

let realAlicePayBob: Transaction = .standard(
    id: "๐Ÿน",
    inputs: [
        .init(
            previousTxID: "๐Ÿ”ฎ",
            previousTxOutputIndex: 0,
            signature: "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ๐Ÿ”๐Ÿน"
        ),
    ],
    outputs: [
        .init(recipientAddress: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ“ฎ", value: "๐Ÿ’ฐ๐Ÿ’ฐ"),
        .init(recipientAddress: "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ๐Ÿ“ฎ", value: "๐Ÿ’ฐ"), // change
    ]
)

Great, now ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ only has ๐Ÿ’ฐ and ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ has ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ in total.

๐Ÿค  hooray your transaction is validated!
Current UTXO pool:
๐Ÿน(0) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ 
๐Ÿน(1) | owner: ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ | amount: ๐Ÿ’ฐ 
๐Ÿ”ฎ(1) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ

Case 3. ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ counterfeits ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ out of thin air

Unfortunately, heโ€™s too lazy to do due diligence so just picks up a random transction ID ๐Ÿฅƒ to locate previous transaction, which never exists.

let bobDayDream: Transaction = .standard(
    id: "๐Ÿธ",
    inputs: [
        .init(
            previousTxID: "๐Ÿฅƒ", // never exists in UTXO pool
            previousTxOutputIndex: 1,
            signature: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ”๐Ÿธ"
        ),
    ],
    outputs: [
        .init(recipientAddress: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ“ฎ", value: "๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ"),
    ]
)

Unsurprisingly, his transaction is denied.

๐Ÿ˜ uh oh your transaction is denied. 
Reason: Input not found in UTXO pool
Current UTXO pool:
๐Ÿน(0) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ 
๐Ÿน(1) | owner: ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ | amount: ๐Ÿ’ฐ 
๐Ÿ”ฎ(1) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ

Case 4. ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ counterfeits ๐Ÿ’ฐ paid by ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ

This time he gets smarter. He looks up a previous existing transaction ๐Ÿน payable to ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ, fakes a signature and put himself as recipient.

let fakeAlicePayBob: Transaction = .standard(
    id: "๐Ÿบ",
    inputs: [
        .init(
            previousTxID: "๐Ÿน",
            previousTxOutputIndex: 1,
            signature: "๐Ÿ‘ท๐Ÿปโ€โ™€๏ธ๐Ÿ”๐Ÿบ" // fake ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ signature signed by ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ
        ),
    ],
    outputs: [
        .init(recipientAddress: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ“ฎ", value: "๐Ÿ’ฐ"),
    ]
)

Butโ€ฆtransaction ๐Ÿน is payable to ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ, so only her signature could redeem it.

๐Ÿ˜ uh oh your transaction is denied. 
Reason: Invalid input signature
Current UTXO pool:
๐Ÿน(0) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ 
๐Ÿน(1) | owner: ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ | amount: ๐Ÿ’ฐ 
๐Ÿ”ฎ(1) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ

Case 5. ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ tries paying ๐Ÿ’ฐ๐Ÿ’ฐ to ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ by redeeming ๐Ÿ’ฐ twice

She needs to pay ๐Ÿฆน๐Ÿป ๐Ÿ’ฐ๐Ÿ’ฐ, but she only owns ๐Ÿ’ฐnow - so sheโ€™s gonna try her luck by redeeming same UTXO payable to her multiple times.๐Ÿ’ฐx 2 = ๐Ÿ’ฐ๐Ÿ’ฐ, what a beautiful math.

let alicePayBobDoubleSpend: Transaction = .standard(
    id: "๐Ÿฅ‚",
    inputs: [
        .init(
            previousTxID: "๐Ÿน",
            previousTxOutputIndex: 1,
            signature: "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ๐Ÿ”๐Ÿฅ‚"
        ),
        .init(
            previousTxID: "๐Ÿน",
            previousTxOutputIndex: 1,
            signature: "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ๐Ÿ”๐Ÿฅ‚"
        ),
    ],
    outputs: [
        .init(recipientAddress: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ“ฎ", value: "๐Ÿ’ฐ๐Ÿ’ฐ"),
    ]
)

โ€œSame UTXO redeemd multiple times in inputsโ€โ€ฆBummer.

Try adding a transaction...
๐Ÿ˜ uh oh your transaction is denied. 
Reason: Same UTXO redeemd multiple times in inputs
Current UTXO pool:
๐Ÿน(0) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ 
๐Ÿน(1) | owner: ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ | amount: ๐Ÿ’ฐ 
๐Ÿ”ฎ(1) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ

Case 6. ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ tries aggregating ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ + ๐Ÿ’ฐ๐Ÿ’ฐ into ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ

He just loses faith that ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ could figure out a way to pay him, so he decides to try something creative again. โ€œWhat if I just aggregate all my funds, and secretly add a little more to it?โ€

let bobAggregateChangesMoreThanHeOwn: Transaction = .standard(
    id: "๐Ÿป",
    inputs: [
        .init(
            previousTxID: "๐Ÿ”ฎ",
            previousTxOutputIndex: 1,
            signature: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ”๐Ÿป"
        ),
        .init(
            previousTxID: "๐Ÿน",
            previousTxOutputIndex: 0,
            signature: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ”๐Ÿป"
        ),
    ],
    outputs: [
        .init(recipientAddress: "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ๐Ÿ“ฎ", value: "๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ"),
    ]
)

OK, fair enough.

๐Ÿ˜ uh oh your transaction is denied. 
Reason: Total input ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ is less than total output ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ
Current UTXO pool:
๐Ÿน(0) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ 
๐Ÿน(1) | owner: ๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ | amount: ๐Ÿ’ฐ 
๐Ÿ”ฎ(1) | owner: ๐Ÿฆน๐Ÿปโ€โ™‚๏ธ | amount: ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ 

It feels like being a bad actor is way much harder than being a good citizen?