Categories
Bitcoin Programming

Working through BUIDL Bootcamp: BankCoin

Problem with ECDSA Coin

The problem with our previous coin is that it could be doublespent. Alice could have sent that coin to anyone before sending it to us. There is no source of truth. We can use centralization to fix this.

Every time a coin is sent, we use a “bank” to make sure it is valid. Alice can’t send a coin to Bob, then send the same coin to Joe.

How We Build Our Bank

The Bank should be able to issue coins to anyone. We can create coins by using a person’s public key.

Our Bank should track the coins we created. This way, we can ensure ownership and transfers and make sure no one double-spends.

We’ll make sure our bank can check everyone’s balance, and that their balance is consistent with the ledger we’ve created.

Also, we’ll have a function that our users can use. This function tells the bank, “Hey, I let Bob have my coin now. Please update your ownership records.”

Building BankCoin

We go through and build the functionality that is described by the tests. This is pretty straightforward.

The deepcopy function allows us to not have side effects in our code. We will add the deepcopied coin to our database of coins, in Bank. For more reading about deepcopy.

If you worked through the extra exercise I talked about previously, this whole coding exercise might be simplistic for you.

In case it isn’t, let’s break it down.

We have 3 main classes: Bank, BankCoin, and Transfer.

Transfer

Transfer is exactly the same as what we constructed in ECDSACoin.

Bank

def __init__(self):
    # coins.id -> coin
    self.coins = {}

Bank is instantiated with a database.

def issue(self, public_key):
        # Create a message specifying who the coin is being issued to
        message = serialize(public_key)

        # Create the first transfer, signing with the banks private key
        transfer = Transfer(
            signature=None,
            public_key=public_key,
        )

        # Create and return the coin with just the issuing transfer
        coin = BankCoin(transfers=[transfer])
        self.coins[coin.id] = deepcopy(coin)
        return coin

We can issue from the bank, and we don’t need a private key, because we trust the bank. The public key is needed, because we have to send it to someone upon issuance.

We then copy the coin to our database of coins, so we can track all the coins.

def fetch_coins(self, public_key):
        coins = []
        for coin in self.coins.values():
            if coin.transfers[-1].public_key.to_string() == public_key.to_string():
                coins.append(coin)
        return coins

We build a fetch coins function. This allows us to check out all the coins that are associated with a given public key.

We’ll query the coins database, and loop through it. We’ll check out each individual coin last transfer, and see if it matches the public key we have. If it does, then that shows ownership and we can add it to our array of coins that belong to a certain person, which we’ll return later, when the function completes.

If this part is confusing to you, I would suggest revisiting object orientation and playing with the code to get a deeper understanding of what is going on here.

def observe_coin(self, coin):
        last_observation = self.coins[coin.id]
        last_observation_num_transfers = len(last_observation.transfers)

        assert last_observation.transfers == coin.transfers[:last_observation_num_transfers]
        coin.validate()

        self.coins[coin.id] = deepcopy(coin)
        return coin

The last function we build is called “observe coin”. This helps us make sure everything is valid and up to snuff.

We take a coin as a parameter, and find it in our database. We then compare the coin in our database to the coin we passed as a parameter. If they match their transfers list, we’ll call “coin.validate()”, then update the existing coin.

BankCoin

The last class we construct in this section is called BankCoin. We essentially have the same exact code here as we did in the ECDSA Coin, except we move the transfer mechanism inside, and delete a small amount of code. Not worthwhile to explain.

If you didn’t understand this part though, it would be a good idea to repeat the previous lesson and check it out. None of this stuff is super easy or trivial, so don’t feel bad if you don’t understand it initially.

By this time, I’ve reached the 38:00 minute mark, and there’s still a lot left to do, even though we just passed all our tests.

Building on BankCoin

Some of the problems with BankCoin includes that we can’t transfer more than 1 coin at a time, we cannot transfer multiple coins to a single person in a single transfer, we cannot transfer 1 coin and split it between multiple people.

This boils down to divisibility. We need to be able to divide up BankCoin. We can do this with UTXO’s.

UTXO

UTXO’s are shorthand for unspent transactions output. It’s probably smart to understand how UTXO’s work, so this link on stackoverflow you should read, also this one was pretty good too.

Basically – UTXO is unspent Bitcoin corresponding to a Bitcoin address. That’s not even technically accurate, but it’s easier to understand, I think.

PS – Here I stumbled across this resource and got incredibly distracted. It’s worth checking out if you’re interested in running a full node, testnode in the cloud.

At any rate — In order to solve the divisibility problem, we introduce the idea of UTXO’s. This causes us to completely throw away all the code we had beforehand, and rewrite it with TX’s in mind.

TX

class Tx:

    def __init__(self, id, tx_ins, tx_outs):
        self.id = id
        self.tx_ins = tx_ins
        self.tx_outs = tx_outs

    def sign_input(self, index, private_key):
        spend_message = self.tx_ins[index].spend_message()
        signature = private_key.sign(spend_message)
        self.tx_ins[index].signature = signature

The first bit of code we write is the TX class. We use this to manage all our tx_ins, and tx_outs.

We also have this function called “sign input”, where we grab a spend message, generate a signature from it, and assign it to a tx_in.

TXIN

class TxIn:

    def __init__(self, tx_id, index, signature):
        self.tx_id = tx_id
        self.index = index
        self.signature = signature

    def spend_message(self):
        return f"{self.tx_id}:{self.index}".encode()

Nothing novel here. We have our TxIn class, with a few attributes. TxIn’s don’t have an amount associated with them, only TxOut’s have amounts.

If we want to find the amount associated with it, we have to grab the txid, and then find the previous associated tx_out with it, then grab the amount from that tx_out. This might be wrong, but as far as I understand, it is correct.

Next, we have a spend_message() function. This is referenced by our original TX class. It just allows us to return a string with variables self.tx_id, and self.index passed into the string, and then encode them in utf-8 encoding. Learn more about this function here.

TXOUT

class TxOut:

    def __init__(self, tx_id, index, amount, public_key):
        self.tx_id = tx_id
        self.index = index
        self.amount = amount
        self.public_key = public_key

Very boring class. I mean, there’s nothing to expand on here.

Bank

class Bank:

    def __init__(self):
        self.txs = {}

    def issue(self, amount, public_key):

        id = uuid4()
        tx_ins = []

        tx_outs = [
            TxOut(tx_id = id, index=0, amount=amount, public_key=public_key)
        ]

        tx = Tx(id=id, tx_ins=tx_ins, tx_outs=tx_outs)
        self.txs[tx.id] = tx
        return tx

    def is_unspent(self, tx_in):
        for tx in self.txs.values():
            for _tx_in in tx.tx_ins:
                if tx_in.tx_id == _tx_in.tx_id and tx_in.index == _tx_in.index:
                    return False
        return True

    def validate_tx(self, tx):
        in_sum = 0
        out_sum = 0

        for tx_out in tx.tx_outs:
            out_sum += tx_out.amount

        for tx_in in tx.tx_ins:
            assert self.is_unspent(tx_in)

            tx_out = self.txs[tx_in.tx_id].tx_outs[tx_in.index]
            public_key = tx_out.public_key
            public_key.verify(tx_in.signature, tx_in.spend_message())
            in_sum += tx_out.amount

        assert in_sum == out_sum

    def handle_tx(self, tx):
        self.validate_tx(tx)
        self.txs[tx.id] = tx

    def fetch_utxo(self, public_key):
        # Find which (tx_id, index) pairs have been spent
        spent_pairs = [(tx_in.tx_id, tx_in.index)
                        for tx in self.txs.values()
                        for tx_in in tx.tx_ins]
        # Return tx_outs associated with public_key and not in ^^ list
        return [tx_out for tx in self.txs.values()
                   for i, tx_out in enumerate(tx.tx_outs)
                       if public_key.to_string() == tx_out.public_key.to_string()
                       and (tx.id, i) not in spent_pairs]

    def fetch_balance(self, public_key):
        utxo = self.fetch_utxo(public_key)
        return sum([tx_out.amount for tx_out in utxo])

Okay. This is a monster and there’s a ton of stuff going on here. Let’s work through this together, eh?

def __init__(self):
        self.txs = {}

This creates our database of TX’s. We throw all our TX’s here, when they’re created.

def issue(self, amount, public_key):

        id = uuid4()
        tx_ins = []

        tx_outs = [
            TxOut(tx_id = id, index=0, amount=amount, public_key=public_key)
        ]

        tx = Tx(id=id, tx_ins=tx_ins, tx_outs=tx_outs)
        self.txs[tx.id] = tx
        return tx

We’ve seen an issue function before, so we basically understand what is going on here. We’re issuing money to someone, we use a few variables of id, index, amount we’re gonna issue and the public key. Nothing really new here, we’ve done this before.

The bonus here, is that we add the TX to the database when we create it. That’s this part of the code:

tx = Tx(id=id, tx_ins=tx_ins, tx_outs=tx_outs)
self.txs[tx.id] = tx
return tx

The next part is us checking what is unspent.

def is_unspent(self, tx_in):
        for tx in self.txs.values():
            for _tx_in in tx.tx_ins:
                if tx_in.tx_id == _tx_in.tx_id and tx_in.index == _tx_in.index:
                    return False
        return True

This is pretty straightforward by itself. We grab our txs database, and iterate through the values, which are individual tx_out’s or tx_in’s.

We then loop through every single tx_in, and check to see if a tx_in in our database corresponds with the tx_in we passed. If there is a correspondence, then we’ve spent it before. If it doesn’t, then it hasn’t been spent.

def validate_tx(self, tx):
    in_sum = 0
    out_sum = 0

    for tx_out in tx.tx_outs:
        out_sum += tx_out.amount

    for tx_in in tx.tx_ins:
        assert self.is_unspent(tx_in)

        tx_out = self.txs[tx_in.tx_id].tx_outs[tx_in.index]
        public_key = tx_out.public_key
        public_key.verify(tx_in.signature, tx_in.spend_message())
        in_sum += tx_out.amount

    assert in_sum == out_sum

Next, we have a validate tx function. It makes sure the TX we pass in is valid, obviously.

We take the TX parameter, iterate through the outs and grab the amount and sum.

Next, we iterate through the tx_ins. tx_ins don’t have an amount, so we have to reference the previous tx_out associated with the tx_in. We then verify the signature, and then add the amount to our variable, “in_sum”.

def handle_tx(self, tx):
    self.validate_tx(tx)
    self.txs[tx.id] = tx

This function runs validate_tx on a tx, then adds or updates it in the db.

def fetch_utxo(self, public_key):
    # Find which (tx_id, index) pairs have been spent
    spent_pairs = [(tx_in.tx_id, tx_in.index)
                    for tx in self.txs.values()
                    for tx_in in tx.tx_ins]
    # Return tx_outs associated with public_key and not in ^^ list
    return [tx_out for tx in self.txs.values()
               for i, tx_out in enumerate(tx.tx_outs)
                   if public_key.to_string() == tx_out.public_key.to_string()
                   and (tx.id, i) not in spent_pairs]

We’re ignoring this function because it’s huge and ugly.

def fetch_balance(self, public_key):
    utxo = self.fetch_utxo(public_key)
    return sum([tx_out.amount for tx_out in utxo])

Fetches utxo, using public key, then grabs balance by iterating over the UTXO’s amount.

Last Refactor

This is the last refactor for the lesson, thank God.

def __init__(self):
    self.utxo = {}

We update the init function in Bank to this, and update throughout out code.

Then, Justin adds a new function, called outpoint. A formal definition can be found here so you can understand what it does. Basically – just references the outputs.

We can refactor other code using this idea of an outpoint, and it makes life a lot easier.

def fetch_utxo(self, public_key):
        return [utxo for utxo in self.utxo.values()
            if utxo.public_key.to_string() == public_key.to_string()]

This code grabs a UTXO for us, by iterating through the list of UTXO’s, and comparing the public key.

I’m done with this lesson right now, and there’s a lot I feel like I understand but a bit that I don’t get, so I’ll just come back to this later and update with the rest of the code.

Leave a Reply