Decentralised lottery with blockchain

By now, you would have heard the phrase "Blockchain technology will completely change industry x" more than once. It's true that blockchain has been used in many industries to great effect. Most people would be familiar with how blockchain has been used in the finance sector with Bitcoin and other cryptocurrencies, but there is also a wide range of other uses for the technology that are starting to transform other industries like supply chain management and healthcare.

One area where I hear the term blockchain getting used increasingly in my industry is when talking about decentralised lotteries. When I start to discuss this topic in a little more detail, people seem to have a broad understanding of what blockchain is, at least as far as how it works with cryptocurrencies, but when pushed about the practical applications for lotteries, they tend to draw a blank.

There are numerous areas in which blockchain could enhance the operations of both traditional and raffle-based lotteries. I thought it would be interesting to spend a little time and explore how the technology could be used to move away from the traditional central gaming system architecture that dominates the lottery sector, replacing this approach with a distributed and decentralised architecture that allows for more transparency and a democratised approach to draw management.

What is a blockchain?

When people think about blockchain, they tend to jump straight to cryptocurrencies, and this association tends to cloud people's understanding of what blockchain as a technology actually is. It's important to understand that cryptocurrency and blockchain are related but distinct concepts. Cryptocurrency is a specific application of blockchain, whereas blockchain is a broader concept that has many other potential uses beyond cryptocurrency.

Before I get into the detail of how blockchain could be used to create a distributed lottery system, its important to take some time to understand the basics of how blockchain as a technology actually works.

Understanding the blockchain

A blockchain is a decentralised and distributed digital ledger that records transactions across a network of computers (nodes) using blocks. Each block in the blockchain represents a single transaction on the blockchain and contains the data for that transaction.

You can think of a block as the data you want to store, with some additional metadata.

Block in a lottery
Example of a block in a blockchain

In a blockchain, each transaction is represented by a block, and each block is linked to the previous block, creating a chain of blocks. The blocks are linked using a mathematical algorithm called a hash witch is a unique digital fingerprint of the data stored in the block, and any change in the block's data or the previous block's hash would result in a completely different hash value.

Block linking in a blockchain

Using the hash to link blocks together in this way ensures the integrity of the blockchain by making it possible to accurately verify the information stored in an individual block by recalculating the hash and confirming that it still matches the one on record.

Understanding blockchain nodes

Put simply, you can think of a blockchain node as a server on a network running an instance of the blockchain software.

If a blockchain existed on a single node only, it would still be vulnerable to manipulation, as it would still be possible for that single node to regenerate the entire chain and recalculate all the hash values.

Blockchain nodes
Blockchain node network

By having multiple independent nodes maintaining a copy of the entire blockchain, the blockchain can be decentralised, meaning no single entity has control over the blockchain. Instead, a consensus on the blockchain's value needs to be agreed upon by the network of nodes.

Decentralisation of the blockchain across multiple nodes ensures the security and integrity of the blockchain. As the validity of any block can be independently verified, any block manipulation by any individual node would be quickly identified by other nodes in the network.

Each node in a blockchain understands how to interact with each other by following a set of protocols and rules that govern the operation of the particular blockchain. Each node does not need to be running the same software; what is essential is there understanding of the protocols and rules.

When a node joins the network, it downloads a copy of the entire blockchain and becomes part of the peer-to-peer network.

Block broadcast flow to nodes

When a node receives a new block from another node, it first checks that the block is valid by verifying the hash of the block. It then compares the hash of the previous block in the chain with the hash stored in the new block to ensure that the block is linked correctly to the chain.

If the block passes all the validation checks, the node broadcasts the block to the rest of the network it knows about, and other nodes will add it to their copy of the blockchain after validating it again themselves.

This process ensures that all nodes have the same copy of the blockchain, and any attempt to modify the chain will be detected and rejected by the network.

The Problem

Modern lotteries or raffles often require multiple jurisdictions, lottery organisers and operators to join together to offer the larger jackpots and prizes required to get customers attention. Lottery operators often form lottery consortiums or associations to run multi-jurisdictional lottery games, which can be more profitable than individual jurisdictions or organisers running their own lottery independently.

The multi-jurisdiction structure involves pooling ticket sales from multiple jurisdictions to create a larger prize pool, which is then shared among the winning players from all participating jurisdictions.

To run multi-jurisdictional lottery games, all the tickets sold by the individual jurisdiction lotteries are collected and stored by them. This data is then forwarded to a central gaming system managed by the lottery consortium or association responsible for the game. The central gaming system processes the ticket data, including verifying the validity of each ticket, ensuring the reconciliation of sales between jurisdictions and calculating the winning numbers for each game.

After the winning numbers are calculated, the lottery consortium or association notifies each participating jurisdiction of the results. The individual jurisdictions are then responsible for paying out the prizes to their respective winners.

The multi-jurisdictional lottery relies heavily on the central gaming system. Any issues with the central gaming system could have serious consequences for the lottery game, including delayed processing of tickets, incorrect calculation of winning numbers, and even the possibility of invalidating genuine lottery entries.

With the central gaming system being solely responsible for calculating the winning tickets, it becomes a significant bottleneck in the speed at which results can be finalised.

The Solution

A blockchain-based lottery system can eliminate the need for a centralised lottery or raffle structure. Since blockchain is a decentralised ledger technology, it does not require a central authority to control or manage the lottery system. Instead, the lottery can be run on a distributed network of nodes, with each node having a copy of the tamper-proof blockchain ledger that contains a record of all transactions on the lottery.

Each lottery draw can be represented by a separate blockchain, which starts with a genesis block containing the rules of the lottery, prize pool and other important information to store about the lottery operation.

Each ticket purchase can be recorded as a block on the blockchain. This block would contain information about the ticket, including the numbers selected, the date of purchase, and the amount paid. Any status changes to a ticket, such as if it was cancelled or refunded, would also be recorded as a new block on the blockchain.

When the lottery results are known, the winning information can be added as another block to the chain. All nodes across the network can then start calculating the winners and committing their result back to the blockchain as another block representing a status update to the ticket and leveraging the power of the network to process results in a much faster and more efficient way.

When winners claim their prizes, another block can be added to the blockchain with updated ticket status. This block could contain information about how the prize was paid, the amount paid, and the payment date.

To further enhance the security of the blockchain-based lottery system, a public-private key pair can be used to sign each transaction on the blockchain. The private key would only be accessible to authorised sources, such as the lottery organisers, or participating jurisdictions, while the public key would be made available as part of the chain. This way, any block can be validated to ensure that it was added by a valid source.

This approach of using public-private keys for signing transactions on the blockchain would allow different independent lottery organisers and operators to hold the private key required to contribute to the lottery securely and transparently. This way, various parties can contribute without any one party needing to be the primary authority or centralised record holder.

Furthermore, government oversight or special interest groups could also hold a complete ledger of entries and independently validate the entries on the blockchain to ensure fairness and transparency without being able to contribute changes themselves.

This approach would create a decentralised and transparent system that provides trust and confidence to players, organisers, and operators alike.

In addition, a public-private key pair can also be used to prove ownership of a ticket on the blockchain. When a ticket is purchased, a new private key can be generated, which would be used to sign the ticket ownership. The private key would then be provided to the ticket owner at the time of purchase, allowing them to use it to prove that they are the rightful owner of the ticket at a later time if they want to claim a prize or cancel the entry.

Creating a new blockchain for each lottery draw would ensure that the data is organised and easy to access and would provide a clear record of all ticket purchases, cancellations, and winnings for that particular draw in perpetuity.

Overall, the distributed nature of blockchain technology offers an innovative and efficient solution to record lottery entries while maintaining integrity and transparency in the system.

Proof of concept?

Now this is primarily a development blog, and I'm passionate about JavaScript, so to continue with our explenation, let's create a little Javascript (NodeJS) proof of concept to help explain the practicalities of using blockchain to decentralise a leger of lotterie entries.

Introduction

To manage the scope for this proof of concept, let's define some parameters we will work within:

  1. We will focus on a single lottery draw.
  2. We want to limit the ability to add entries to the blockchain to only authorised nodes but still allow the chain to propagate to other nodes for validation.
  3. We want to ensure that the valid owner of an entry can be validated on any node.
  4. We will only focus on the initial purchase status, not allowing entries to be cancelled or prizes claimed.
  5. We will not add any complicated mechanics to manage race conditions across the network, simply rejecting a block if this edge case does eventuate.
  6. We will store our blockchain in a simple JSON object to be easily serialisable and only maintain it in memory.
  7. We will create a simple API to add, view and validate ownership of enteries in our lottery.

We will tackle this in two parts:

  1. Create the classes to manage the blockchain data structure.
  2. Create a NodeJs express server to act as our blockchain nodes and manage block addition, validation and propagation. The server (nodes) will use sockets to communicate with each other and a simple REST API to contribute to the chain.

The socket connection in the proof of concept will be kept simple, not allowing for loss of connection and reconnection to the network.

Creating the Block Class

As the name suggests, a blockchain comprises a series of connected Blocks. The blocks are linked in the "chain" in chronological order using a hash of the Block.

The hash is calculated using several pieces of metadata about the Block, including the hash of the previous Block in the chain, a timestamp and the transaction data we are storing on the Block.

Including the previous Block's hash in the new Block's hash calculation is essential, as it makes it extremely difficult to modify the blockchain. If a malicious actor wanted to change a previous block, they would need to recalculate the hash for that Block and every subsequent Block in the chain.

Each Block also contains a hash of the previous Block, which creates a chain of blocks linked together by these hash values and can be used to validate other blocks in the chain.

We are also adding two security measures to our blockchain:

Each Block is signed by the node that created the Block, verifying that they are authorised to add blocks to the chain; only nodes with access to the private key used when establishing the blockchain will be able to create a valid signature.

The public key will be stored against the blockchain so that any other node can verify new Blocks have been created by an authorised node before adding them to the chain.

Each Block also contains an owner signature created at the time of block creation. The private key used to create this signature is provided back to the owner of the Block or, in this case, lottery ticket entry as proof of ownership of the Block or lottery ticket entry.

/**
 * Represents a block in a blockchain.
 * 
 * The purpose of the Block class is to represent a single transaction on the blockchain. 
 * Each block in the blockchain stores a hash of the block's data, and is linked to other blocks via a hash of the previous block's data.
 * 
 *  Block class has the following properties:
 * 
 * - index: The index of the block in the blockchain. This is used to easily identify the order of the blocks in the chain without needing step through all the hash links
 * - timestamp: The timestamp of the block, which represents the time when the block was created.
 * - data: The data that is contained in the block. This could be a set of transactions, lottery entry details, or any other information that needs to be stored in the blockchain.
 * - previousHash: The hash of the previous block in the chain. This is used to link the blocks together in the chain and maintain the integrity of the blockchain.
 * - hash: The hash of the current block. This is calculated based on the data in the block, timestamp and previousHash; and is used to ensure the integrity of the block and prevent tampering.
 * - signature: The signature the block creator to provide proof that the block was created by an authorised source
 * - owner: The signature of the block owner, used to validate ownership of the block to claim prizes or create additional transaction on the block
 */
class Block {
    constructor(index, timestamp, data, previousHash, publicKey) {
        this.index = index;
        this.timestamp = timestamp;
        this.data = data;
        this.previousHash = previousHash;
        this.hash = this.calculateHash();
        this.publicKey = publicKey;
        this.signature = null;
        this.owner = null;
    }

    /**
     * This method calculates and returns the SHA256 hash of the block's data, timestamp, and previous hash.
     * The hash is stored in the hash property of the block.
     * 
     @returns {string} The calculated SHA256 hash.
    */
    calculateHash() {
        this.hash = CryptoJS.SHA256(
            this.previousHash +
            this.timestamp.toString() +
            JSON.stringify(this.data)
        ).toString();

        return this.hash;
    }

    /**
     * Signs the block to provide proof that the block was created by an authorised source using the provided private key.
     * @param {string} privateKey - The private key used to sign the block.
     */
    signBlock(privateKey) {
        this.signature = (privateKey) ? privateKey.sign(this.calculateHash(), "base64") : null;
    }

    /**
     * Verifies the block's signature and data using the provided public key.
     * 
     * @param {string} publicKey - The public key used to verify the block's signature.
     * @returns {boolean} True if the signature is valid, false otherwise.
     */
    isValidBlock(publicKey) {
        if (!this.signature) return false;
        return publicKey.verify(this.calculateHash(), this.signature, "utf8", "base64");
    }

    /**
     * Marks the block as owned by the provided private key. Only someone with access to the private key can prove ownership of this block. 
     *  
     * @param {string} key - The key used to mark the block as owned.
     */
    markOwner(key) {
        this.owner = key.sign(this.calculateHash(), 'base64');

    }

    /**
     * Verifies the block's owner using the provided private key.
     * 
     * @param {string} key - The private key used to verify the block's owner.
     * @returns {boolean} True if the owner is valid, false otherwise.
     */
    verifiedOwner(key) {
        // Verify the signature using the public key
        return key.verify(this.calculateHash(), this.owner, 'utf8', 'base64');
    }

    /**
     * Deserializes a block back into a Block object.
     * 
     * @param {Object} serializedBlock - The serialized block to be deserialized.
     * @returns {Block} The deserialized Block object.
     */
    static deserialize(serializedBlock) {
        const { index, timestamp, data, previousHash, hash, publicKey, signature, owner } = serializedBlock;
        const block = new Block(index, timestamp, data, previousHash, publicKey);
        block.signature = signature;
        block.owner = owner;
        block.hash = hash;
        return block;
    }

    /**
     * Serializes the block into a plain JSON object.
     * 
     * @returns {Object} The serialized block.
     */
    serialize() {
        return {
            index: this.index,
            timestamp: this.timestamp,
            data: this.data,
            previousHash: this.previousHash,
            hash: this.hash,
            publicKey: this.publicKey,
            signature: this.signature,
            owner: this.owner
        };
    }
}

Creating the Blockchain Class

The Blockchain class is responsible for managing and verifying the integrity of the chain of Block objects.

The Blockchain class has a constructor that initialises the chain with a single block called the genesis block. In this example, we use the genesis block to store information about the lottery draw, including game rules.

The constructor function also takes two optional arguments publicKey, and privateKey:

  • publicKey is used to sign new blocks to validate their authenticity among other nodes on the network. If no privateKey is specified, new blocks are not able to be signed and will be rejected by other nodes on the network.
  • The publicKey is serialised as part of the Blockchain, allowing other nodes to use it to validate the authenticity of blocks shared over the network. If a publicKey is provided, it will be used to generate a privateKey

The Blockchain class has several methods that are used to manage the chain. For example, the addBlock() method is used to add a new block to the chain, and the addBlockFromNetwork() method is used to add existing blocks, once validated, that have been shared on the network by other nodes.

The isValid() method checks if the chain is valid by validating each block's hash value and previous hash value against the previous block's hash value. It returns a boolean value indicating whether the chain is valid.

The serialised() method returns a serialised blockchain used to share the chain with other nodes on the network. It uses the Block class's serialise() method to serialise each block in the chain.

The static deserialise() method takes a serialised blockchain object published over the network and returns a Blockchain instance with the corresponding chain.

/**
 * A class representing a blockchain.
 */
class Blockchain {

    /**
     * Constructs a new blockchain instance.
     * 
     * @param {any} [genesisData=null] The data for the genesis block.
     * @param {string} [publicKey=null] The public key used verify block signature blocks.
     * @param {string} [privateKey=null] The private key used to sign blocks.
     */
    constructor(genesisData = null, publicKey = null, privateKey = null) {
        this.privateKey = privateKey;
        this.publicKey = publicKey;
        this.chain = [this.createGenesisBlock(genesisData)];
    }

    /**
     * Creates the genesis block for the blockchain.
     * 
     * @param {any} [data=null] The data to include in the block.
     * @returns {Block} The genesis block.
     */
    createGenesisBlock(data = null) {
        let block = new Block(0, new Date().toString(), data || "Genesis Block", "0");
        block.hash = block.calculateHash();
        block.signBlock(this.privateKey);
        return block;
    }

    /**
     * Gets the latest block in the blockchain.
     * 
     * @returns {Block} The latest block.
     */
    getLatestBlock() {
        return this.chain[this.chain.length - 1];
    }

    /**
     * Adds a new block to the blockchain.
     * 
     * @param {any} data The data to include in the block.
     * @param {string} [key=null] The public key of the block owner.
     * @returns {Block} The new block.
     */
    addBlock(data, key = null) {
        const newBlock = new Block(this.chain.length, new Date().toString(), data);
        newBlock.previousHash = this.getLatestBlock().hash;
        newBlock.hash = newBlock.calculateHash();
        newBlock.signBlock(this.privateKey);
        newBlock.markOwner(key);
        this.chain.push(newBlock);
        return newBlock;
    }

    /**
     * 
     * @param {*} targetHash 
     */

    /**
     * Removes blocks from the end of the chain until a block with the specified hash is found. We want to remove all items from the 
     * end of the array until an object with a matching hash value is found. 
     * 
     * @param {string} targetHash The hash of the block to remove.
     * @returns {boolean} `true` if a block was removed, `false` otherwise.
     */
    removeBlock(targetHash) {
        let removed = false;
        while (this.chain.length > 0) {
            const lastItem = this.chain[this.chain.length - 1];
            if (lastItem.hash === targetHash) {
                this.chain.pop();
                removed = true;
                break;
            } else {
                this.chain.pop();
                removed = true;
            }
        }
        return removed;
    }

    /**
     * 
     * This method is called addBlockFromNetwork, and it takes a Block object as a parameter. Here's how it works:
     * 
     *
     * @param {*} block 
     * @returns 
     */

    /**
     * Adds an existing block to the chain that was received from the network.
     * 
     * First we checks that the index of the block is valid. The index should be one greater than the index of the latest block in the chain.
     * Next we checks that the previous hash of the block is valid. The previous hash should be equal to the hash of the latest block in the chain.
     * Then we checks that the hash of the block is valid. It does this by calling the calculateHash method on the block and comparing the result to the hash property of the block.
     * If all the checks pass, the block is added to the chain by calling the push method on the chain array, and a message is logged to the console indicating that the block has been added.
     * If any of the checks fail, the method returns without adding the block to the chain, and a message is logged to the console indicating the reason for the failure.
     * 
     * @param {Block} block The block to add to the chain.
     * @returns {boolean} `true` if the block was added, `false` otherwise.
     */
    addBlockFromNetwork(block) {
        if (block.index !== this.getLatestBlock().index + 1) {
            console.error("Block index is not valid");
            return false;
        }

        if (block.previousHash !== this.getLatestBlock().hash) {
            console.error("Block previous hash is not valid");
            return false;
        }

        if (block.hash !== block.calculateHash()) {
            console.error("Block hash is not valid");
            return false;
        }

        this.chain.push(block);
        return true;
    }

    /**
     * Checks if the blockchain is valid.
     * 
     * @returns {boolean} `true` if the
     */
    isValid() {
        for (let i = 1; i < this.chain.length; i++) {
            const currentBlock = this.chain[i];
            const previousBlock = this.chain[i - 1];

            if (currentBlock.hash !== currentBlock.calculateHash()) {
                return false;
            }

            if (currentBlock.previousHash !== previousBlock.hash) {
                return false;
            }
        }

        return true;
    }

    /**
     * Creates a new instance of the Blockchain class from a serialized representation.
     * 
     * The method expects an object with a 'chain' and 'publicKey' properties.
     * The 'chain' property should be an array of serialized Block objects.
     * The 'publicKey' property should be a string representing the public key of the blockchain.
     * @param {Object} serializedBlockchain - An object with a 'chain' and 'publicKey' properties.
     * @returns {Blockchain} A new instance of the Blockchain class with the properties and blocks extracted from the serialized representation.
     */
    static deserialize(serializedBlockchain) {
        const { chain, publicKey } = serializedBlockchain;

        const blockchain = new Blockchain();
        blockchain.publicKey = publicKey;
        blockchain.chain = chain.map((block) => Block.deserialize(block));
        return blockchain;
    }

    /**
     * Returns a serialized representation of the current instance of the Blockchain class.
     * 
     * The method returns an object with a 'publicKey' and 'chain' property.
     * The 'publicKey' property is a string representing the public key of the blockchain.
     * The 'chain' property is an array of serialized Block objects.
     * @returns {Object} An object with a 'publicKey' and 'chain' property representing the serialized Blockchain.
     */
    serialize() {
        return {
            publicKey: this.publicKey,
            chain: this.chain.map(block => block.serialize())
        };
    }
}

Test Blockchain Data structure

Let's test our blockchain data structure by creating a little example of the blockchain data structure and simulating some actions on it.

First, we generate a public and private key pair using the NodeRSA library. Then, we need to create an example lottery object and initialize a new blockchain with the lottery object, public key, and private key.

// Create a publick private key pair to sign our block
const privateKey = new NodeRSA({ b: 512 });
const publicKey = privateKey.exportKey("public");

// Creare out lottery to be stores in the genesis node
const exampleLottery = {
  name: "Powerball",
};

const blockchain = new Blockchain(exampleLottery, publicKey, privateKey);

Next, we create a ticket object with an array of numbers and generate new private keys using the NodeRSA library for the ticket and a fake one we can use to test the validity. We can now add the new block to the blockchain.

// Create a
const ticket = {
  numbers: [1, 2, 3, 4, 5, 6],
};

// Create new keys to asign ownership to the block
const ticketPrivateKey = new NodeRSA({ b: 512 });
const fakePrivateKey = new NodeRSA({ b: 512 });

let newBlock = blockchain.addBlock(ticket, ticketPrivateKey);

After adding a block, we can verify the ownership of the ticket by checking if the ticket's private key can be used to decrypt the block's signature. We can also test the fake key we created to test that the fake key is not able to be used to decrypt the signature using the fake private key.

// Verify owner
console.log("Validate Ownership", newBlock.verifiedOwner(ticketPrivateKey));

// Verify owner
console.log("Fake Ownership", newBlock.verifiedOwner(fakePrivateKey));

Let's check if the blockchain is valid by calling the isValid() method on the blockchain object.

// Validate the blockchain
console.log("Valid Blockchain", blockchain.isValid());

Finally, we can simulate a malicious attack by manipulating the data in the second block of the blockchain. It changes the numbers in the ticket object and then checks if the blockchain is still valid. The output of the isValid() method call will be false since the data in the second block has been tampered with.

// simulate minisusley manipulate the data on the blockchain
blockchain.chain[1].data = {
  numbers: [6, 5, 4, 3, 2, 1],
};

console.log("Valid Blockchain", blockchain.isValid());

Creating the Blockchain Node Server

To utilise our blockchain data structure, we need to create our server that manages the blockchain protocols and rules and handles updating and broadcasting of changes across the network.

The BlockchainNode class represents a node in a blockchain network that can add and verify blocks, communicate with other nodes in the network, and broadcast messages.

The constructor function takes a configuration object as input and initializes the node.

The options object can contain the following properties:

  • port: the port number to use for the connection
  • host: the host address for the connection. If not provided, it defaults to localhost.
  • privateKey: the private key to use for authentication. If not provided, it generates a new key using the NodeRSA library.
  • verbose: a boolean that enables verbose logging if set to true.

The BlockchainNode class has several methods that are used to initialize various parts of the node:

  • initializeBlockchain(): initializes the blockchain with some default values if the node is the first node in the network. If a private key is not provided, it generates a new key using the NodeRSA library.
  • initializeApiServer(port): initializes an API server to allow the node to receive and respond to HTTP requests from clients.
  • initializeDownstreamServer(): initializes a WebSocket server to allow the node to communicate with other nodes in the network.
  • initializeUpstreamClient(host): initializes a WebSocket client to connect to another node in the network.

In addition, the class has a processMessage() method that is used to process messages received from other nodes in the network. It takes two parameters: message, which is the message received, and socket, which is an optional parameter representing the socket object that the message was received from and which can be used to send messages back to the node if needed.

The method returns a boolean value, indicating whether the message was successfully processed or not.

The processMessage method is responsible for handling various types of messages that can be received from the node network, including "add" messages when a new block is added to the blockchain, "sync" messages when a new node connects and needs to synchronize with the blockchain, or possibly even "ping" messages to check if a node is still alive.

class BlockchainNode {

    /**
    * Creates a new instance of the client constructor.
    * @constructor
    * @param {object} options - The configuration options for the client.
    * @param {number} options.port - The port number to use for the connection.
    * @param {string} [options.host='localhost:3000'] - The host address for the connection. Defaults to 'localhost:3000' if not provided.
    * @param {string} [options.privateKey=null] - The private key to use for authentication. Defaults to null if not provided.
    * @param {boolean} [options.verbose=false] - Whether to enable verbose logging. Defaults to false if not provided.
    */
    constructor({ port, host = 'localhost', privateKey = null, verbose = false }) {
        this.data = {}; // The blockchain data of the node.
        this.privateKey = privateKey; // The private key of the node for authentication.
        this.verbose = verbose; // Whether to enable verbose logging.

        if (!host) {
            this.initializeBlockchain();
        } else {
            this.initializeUpstreamClient(host);
        }

        this.initializeApiServer(port);
        this.initializeDownstreamServer();
    }

    /**
     * 
     * Initializes the blockchain data of the node.
     * @private
     * */
    initializeBlockchain() {
        const lottery = {
            lottery: "BlockLotto",
            draw: 1,
            creationDate: new Date(),
            prizePool: {
                value: 1000000,
                currency: "AUD"
            },
            rules: {
                description: "Each set of numbers on your ticket is made up of 6 numbers, which represents one game and gives you one chance to win a prize. Numbers will be between 1 and 45 inclusive. 6 winning numbers will be selected at random from the 45 to represent the winning numbers",
                divisions: {
                    1: "All 6 numbers",
                    2: "5 numbers",
                    3: "4 numbers",
                    4: "3 numbers",
                }
            },
        }

        if (!this.privateKey) {
            this.privateKey = new NodeRSA({ b: 512 });
        }

        this.publicKey = this.privateKey.exportKey("public");
        this.data = new Blockchain(lottery, this.publicKey, this.privateKey);
    }

    /**
     * Initializes the API server of the node.
     * 
     * @param {number} port - The port number to use for the API server.
     * @private
     * */
    initializeApiServer(port) {
        const app = express();

        app.use(express.static('public'));
        app.use(express.json());

        /*
        Add a new block to the blockchain.
        @name POST /api/block
        @function
        @param {object} req - The request object.
        @param {object} res - The response object.
        @param {string} req.body.name - The name of the block owner.
        @param {Array} req.body.numbers - An array of numbers chosen by the block owner.
        */
        app.post('/api/block', (req, res) => {
            const { name, numbers } = req.body;

            if (!name || !numbers) return res.status(400).send('Name and number fields are required');

            // Key used to verify ownership of this block
            const key = new NodeRSA({ b: 512 });
            const newBlock = this.data.addBlock({ name, numbers }, key);

            let message = new Message("add", newBlock.serialize())
            this.broadcast(message);

            if (this.upstream && this.upstream.OPEN) {
                this.upstream.send(message.toJSON());
            }

            // Serialize private key
            const privateKeySerialized = key.exportKey('pkcs8-private-pem');

            res.status(201).send({ key: privateKeySerialized, hash: newBlock.hash });
        });

        app.get('/api/block', (req, res) => {
            res.status(200).send(this.data.serialize());
        });

        app.post('/api/block/owner', (req, res) => {
            const { hash, key } = req.body;

            // Deserialize private key
            try {
                const deserializedKey = new NodeRSA(key);
              } catch (error) {
                res.status(200).send({
                    owner:false
                });
                return false;
              }

            const block = this.data.chain.find(block => block.hash === hash);
            const owner = (block) ? block.verifiedOwner(deserializedKey) : false;

            res.status(200).send({
                owner:owner
            });
        });

        this.server = app.listen(port, () => {
            console.log(`server started on port ${port}`);
        });
    }

    /**
     * Initialize the WebSocket server to communicate with downstream clients.
     * 
     * It creates a new WebSocket server instance, binds it to the provided HTTP server,
     * and registers event handlers for client connections, messages, and disconnections.
     * On connection, a synchronization message containing the current data state is sent to the client.
     * On message, the received message is processed, broadcasted to all connected clients (except sender),
     * and forwarded to the upstream server if available and open.
     * On disconnection, the client is logged as disconnected.
     * */
    initializeDownstreamServer() {
        this.downstream = new WebSocket.Server({ server: this.server });

        this.downstream.on('connection', socket => {
            console.log('new client connected');

            const message = new Message('sync', this.data.serialize());
            socket.send(message.toJSON());

            socket.on('message', message => {
                if (this.processMessage(message, socket)) {
                    let newMessage = Message.deserialize(JSON.parse(message));
                    this.broadcast(newMessage, socket);

                    if (this.upstream && this.upstream.OPEN) {
                        this.upstream.send(newMessage.toJSON());
                    }
                }
            });

            socket.on('close', () => {
                console.log('a client disconected');
            });
        });
    }

    /**
     * Initializes the WebSocket client that connects to the upstream server
     * 
     * @param {string} host - The host address to connect to
     * @throws {Error} If the provided host address is not a string or is empty
     * @return {void}
     */
    initializeUpstreamClient(host) {
        console.log(`connecting to host at ${host}`);

        // Connect to the server
        this.upstream = new WebSocket(`ws://${host}`);

        this.upstream.on('open', () => {
            console.log(`Connected to server at host ${host}`);
        });

        // Receive messages from the server
        this.upstream.on('message', (message) => {
            this.processMessage(message, this.upstream);

            let newMessage = Message.deserialize(JSON.parse(message));
            this.broadcast(newMessage);
        });
    }


    /**
     * Process a message received from a network node.
     * 
     *  @param {string} message - The message received from the network.
     * @param {object} socket - The socket object the message was received from, used to send messages back to the node if needed. Defaults to null.
     * @returns {boolean} - Returns true if the message was successfully processed, false otherwise.
    */
    processMessage(message, socket = null) {
        let parsedMessage = JSON.parse(message);
        (this.verbose) ? console.log('Message received: ' + parsedMessage.type, parsedMessage.data) : null;

        switch (parsedMessage.type) {
            case 'add':
                const networkNode = Block.deserialize(parsedMessage.data);
                if (networkNode.isValidBlock(new NodeRSA(this.data.publicKey))) {
                    this.data.addBlockFromNetwork(networkNode);
                } else {
                    (this.verbose) ? console.log("Invalid block rejected") : null;
                    let newMessage = new Message('reject', parsedMessage.data);
                    socket.send(newMessage.toJSON());//send a message back about the invalid
                    return false;
                }

                break;
            case 'sync':
                const networkBlockchain = Blockchain.deserialize(parsedMessage.data);
                networkBlockchain.privateKey = this.privateKey; // We need to add the local private key back in, it is not serialize
                this.data = networkBlockchain;
                break;
            case 'reject':
                const badNode = Block.deserialize(parsedMessage.data);
                const { hash } = badNode;
                (this.verbose) ? console.log("rejected block received " + hash) : null;

                let removeBlock = this.data.removeBlock(hash);
                break;
            default:
                console.error('Invalid message type received:', parsedMessage.type);
                return false;
                break;
        }
        return true;
    }

    /**
     * Publish a new value to the blockchain data store.
     * 
     * @param {*} value 
     */
    publish(value) {
        (this.verbose) ? console.log('Data changed: ' + JSON.stringify(value)) : null;

        this.data = value;
    }

    /**
     * Broadcast the current data object to all connected clients
     * 
     * This function takes in a message object and a socket object (optional) and broadcasts the message to all clients except the provided socket.
     * @param {Message} message - A message object to broadcast
     * @param {WebSocket|null} socket - A socket object to exclude from broadcasting (optional)
     * @returns {void} 
    */
    broadcast(message, socket = null) {
        const data = message.toJSON();

        this.downstream.clients.forEach(client => {
            if (socket !== client) { // ignore a particular client
                client.send(data);
            }
        });
    }

}

Creating the Server

Finally we need to create a script to initialise a signle instance of our BlockchainNode and handle the the different configeration management.

The script uses the Commander library to parse command-line arguments. The available options are:

  • -p, --port <port>: The port to run the BlockchainNode on. If not specified, the default value is 3000.
  • -h, --host <host>: The hostname or IP address of the node to connect to.
  • -v, --verbose <verbose>: Display detailed processing information. If not specified, the default value is false.
  • -k, --key <key>: Private key used to validate a new block added to the blockchain.
  • -s, --save <save>: Save the private key to file.

The program.parse(process.argv) line parses the command-line arguments and sets them as options in the options object.

If the key option is specified, the script reads the private key from a file in PKCS#8-encoded PEM format using the fs library. It then creates a new NodeRSA object from the private key.

If the key option is not specified, the script creates a new NodeRSA object with a key length of 512 bits. If the save option is specified, the private key is saved to a file in PKCS#8-encoded PEM format using the fs library.

Finally, the script creates a new BlockchainNode object with the specified options and starts the server.

program
    .option('-p, --port <port>', 'The port to run the BlockchainNode on', parseInt)
    .option('-h, --host <host>', 'The hostname or IP address of the node to connect to')
    .option('-v, --verbose <verbose>', 'Display detailed processing information')
    .option('-k, --key <key>', 'private key used to validate an new block added to the blockchain')
    .option('-s, --save <save>', 'Save the private key to file');

program.parse(process.argv);

const options = program.opts();

const port = options.port || 3000;
const host = options.host || null;
const verbose = options.verbose || false;
const key = options.key || null;
const save = options.save || null;

let privateKey = null;

// Load the private key from a file in PKCS#8-encoded PEM format
if (key) {
    const privateKeyPEM = fs.readFileSync(key, 'utf8');

    // Create a new NodeRSA object from the private key
    privateKey = new NodeRSA(privateKeyPEM);
} else {
    privateKey = new NodeRSA({ b: 512 });

    if (save) {
        // Log out key to be saved
        console.log(privateKey.exportKey('pkcs8-private-pem'));
        // Save the private key to a file in PKCS#8-encoded PEM format
        fs.writeFileSync('private_key.pem', privateKey.exportKey('pkcs8-private-pem'));
    }
}

const server = new BlockchainNode({ port:port, host:host, privateKey:privateKey, verbose:verbose });

Now we can instanciate a single blockchain node like this.

node ./server.js -p 3001 -k ./private_key.pem -v true
cc

And we could create a second node in read only mode and connect it to the network like this.

node ./server.js -p 3002 -h localhost:3001

Putting it all together

Now let's create a small blockchain network example and process some tickets on it.

We will create four nodes, two of which have permission to create new blocks (tickets) in the lottery. And two in read-only mode. For the example, we will run all the nodes on the same local host for convenience on ports 3001, 3002, 3003 and 3004. In the real world, these should be in different data centres with different public domains and access controls.

Initialise node 1:

This is our initial node. It will be responsible for setting up the initial state of the lottery and the blockchain. This note will be able to write to the blockchain.

node ./server.js -p 3001 -k ./private_key.pem -v true

Initialise node 2:

This will be a read-only node with an upstream connection to node 1

node ./server.js -p 3002 -h localhost:3001

Initialise node 3:

This will be our second node with write permission, it will also have an upstream connection to node 1, and we will set this up in verbose mode, so we can see what is happening.

node ./server.js -p 3003 -h localhost:3001 -k ./private_key.pem -v true

Initialise node 4:

Our final node will be another read-only node with an upstream connection to node 3. We will also set this up in verbose mode, so we can see what is happening.

node ./server.js -p 3004 -h localhost:3003 -v true

Now we have a blockchain network with four nodes running.

Now we can purchase a ticket from node 3 using the Rest API

curl --location 'localhost:3003/api/block' \
--header 'Content-Type: application/json' \
--data '{
    "name":"Levi Putna",
    "numbers": [[1,2,3,4,5,6],[6,5,4,3,2,1]]
}'

In response, we would receive a copy of our private key we can use to validate ownership of the block (ticket) at a later date on any other node in the network.

{
    "key": "-----BEGIN PRIVATE KEY-----\nMIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtVeYny/bYr+7yv4B\n9PfUWJqffhyC29pO8AJgziXy501fqRTsg1COgizjvQ946cq2YWvyHBa9xDeA7WXe\nB3drcwIDAQABAkBIiv8yByoDKeJNBSnxPkcDHu/Yuj0bCAz19G2XahTutBbcl5Lp\ncI0u2SphFYrrR7UShHBgw5LEoHO9ikJgMTAhAiEA7p0jly3meCmm3V/sZk+4HxTB\nU0fU3JL/+ucWoFE8YMMCIQDCjitwGTN1LR3BcUNBxtFjkusO9GcvajsGhJEJQG8f\nkQIhAJmR0u0ionjlvbouWVRDrGl8jywNSAcqKMuPXPWTMvvHAiAZRBtSCIPNQNmv\naIUigq5orwjFvWm1F6eotgib2flUcQIhAJt09URAPCmUMsnVG1NfCWoqsFJeAHTv\nkaX11Ay6wA5K\n-----END PRIVATE KEY-----",
    "hash": "b11fd155c05442baff5f0deed0fd607eaa91a903633d7faa25931d010c8f8795"
}

We can also see that the new block (ticket) has been replicated across the other nodes in the network. Each node would have verified that the block came from an authorised source and validated it before adding it to their blockchain version. They would then broadcast the new block out to all the other nodes they know about.

To show that the ticket has been replicated, we can verify ownership of the ticket on node 2 using the private key provided.

curl --location 'localhost:3002/api/block/owner' \
--header 'Content-Type: application/json' \
--data '{
    "key": "-----BEGIN PRIVATE KEY-----\\nMIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtVeYny/bYr+7yv4B\\n9PfUWJqffhyC29pO8AJgziXy501fqRTsg1COgizjvQ946cq2YWvyHBa9xDeA7WXe\\nB3drcwIDAQABAkBIiv8yByoDKeJNBSnxPkcDHu/Yuj0bCAz19G2XahTutBbcl5Lp\\ncI0u2SphFYrrR7UShHBgw5LEoHO9ikJgMTAhAiEA7p0jly3meCmm3V/sZk+4HxTB\\nU0fU3JL/+ucWoFE8YMMCIQDCjitwGTN1LR3BcUNBxtFjkusO9GcvajsGhJEJQG8f\\nkQIhAJmR0u0ionjlvbouWVRDrGl8jywNSAcqKMuPXPWTMvvHAiAZRBtSCIPNQNmv\\naIUigq5orwjFvWm1F6eotgib2flUcQIhAJt09URAPCmUMsnVG1NfCWoqsFJeAHTv\\nkaX11Ay6wA5K\\n-----END PRIVATE KEY-----",
    "hash": "b11fd155c05442baff5f0deed0fd607eaa91a903633d7faa25931d010c8f8795"
}'

Conclusion

Blockchain has the potential to revolutionise the way we think about the storage of lottery entries and removing the need for a central gaming system.

While the concept of using blockchain for lotteries is relatively straightforward, it can become complex when the nodes need to support more complex lottery rules. However, with the use of smart contracts, blockchain can provide a transparent, tamper-proof, and fair lottery system.

In a production situation, there is still a need to provide other connected systems to handle other functions of the lottery, like facilitating the sale of tickets, including financial transactions, keeping track of customer information, storing customers' private keys against their accounts and selecting winning numbers or entries.

Overall, blockchain has the potential to create a more distributed, transparent, secure, and fair lottery system that can benefit both the players and the lottery providers. By leveraging the power of blockchain, the lottery industry can take a step towards a more decentralised and democratic future.

If you're interested in trying out the concept on your own or building upon what I've already started, I am happy to be able to share my working code with you through Github. https://github.com/levi-putna/lotto-blockchain-example. The code discussed in this article has been tagged under the proof-of-concept tag As I may look to extend upon this concept later.

You've successfully subscribed to Twisted Brackets
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.