«  9.7. Consensus in Distributed Systems   ::   Contents   ::   10.1. C Language Reintroduction  »

9.8. Extended Example: Blockchain Proof-of-Work

A blockchain is a sequence of messages that create a verifiable ordering of events. Blockchains can be used in a variety of applications, such as cryptocurrency (Bitcoin) or other distributed database ledgers. One key idea behind a blockchain is that new entries are difficult to generate. A common technique is to require a proof-of-work calculation, such as finding a string that produces a cryptographic hash value with several leading 0s.

Consider the example output below. Each line contains a message index (0, 1, 2, …) followed by the previous line’s hash value (0000..., f74d..., and so on), a timestamp (omitted for brevity), then a log message (“Genesis Block”). The line then ends with an integer value (called a nonce), such as 0000, 22d75, and 6ea as shown. The entire line ending at the nonce is then used as input to a cryptographic hash function. If the output of the hash does not begin with 0s, the nonce is increased and the line is re-hashed. For instance, the second line required 142,709 (0x22d75) attempts before the hash output began with 0s. Once this entry is found, it can be added to the blockchain.

1
2
3
4
5
6
7
8
$ ./blockchain
Here is the blockchain:
  ...0 00000000... [...] Genesis Block ...000000 f74d3a10...
  ...1 f74d3a10... [...] Data message 1 ...022d75 0000b38b...
  ...2 0000b38b... [...] Data message 2 ...0006ea 0000e2c4...
  ...3 0000e2c4... [...] Data message 3 ...00be80 00002c45...
  ...4 00002c45... [...] Data message 4 ...019e63 0000a65e...
  ...5 0000a65e... [...] Data message 5 ...0188a8 0000cb6f...

The second field in each line is the previous line’s hash output, which serves the purpose of declaring that the new line is the only legitimate successor of the previous line. That is, each line explicitly ties itself to the previous line by including the previous line’s hash in the input. Note that the first line, often called a Genesis block, is special, as it does not follow any entry. As such, a nonce of 0 is often used for this value and the hash value does not need to begin with 0s. In this extended example, we use SHA-1 as a cryptographic hash function simply to make the search more efficient. In practice, SHA-1 is considered insecure and should not be use for applications like this.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#include <assert.h>
#include <inttypes.h>
#include <openssl/sha.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define ASCTIME_LENGTH 24
#define BLOCKCHAIN_ZEROES 3

char * construct_block (uint64_t, char *, struct tm *,
                        char *, size_t, uint64_t, size_t *);
char * create_genesis_block (void);
char * hash_to_string (uint8_t *hash);
char * append_block (char *, char *, size_t);

int
main (void)
{
  size_t number_of_threads = 4;
  size_t number_of_blocks = 10;

  /* Generate the blockchain pointers and create genesis block */
  char *blockchain[number_of_blocks];
  memset (blockchain, 0, sizeof (blockchain));
  blockchain[0] = create_genesis_block ();

  /* Use a generic message to append each time */
  char *message = calloc (50, sizeof (char));
  size_t i;
  for (i = 1; i < number_of_blocks; i++)
    {
      strncpy (message, "Data message ", 50);
      snprintf (message + strlen (message), 50 - strlen (message), "%zd", i);
      blockchain[i] =
        append_block (message, blockchain[i-1], number_of_threads);
    }
  free (message);

  /* Print the results of the computations */
  printf ("Here is the blockchain:\n");
  for (i = 0; i < number_of_blocks; i++)
    {
      printf ("  %s\n", blockchain[i]);
      free (blockchain[i]);
    }

  return EXIT_SUCCESS;
}

/* Parameters for each copy of the mining thread */
struct mining_params {
  char *block;
  size_t blocklen;
  uint64_t nonce;
  size_t attempts;
  bool success;
};

/* Blockchain mining thread. Each instance of this thread starts
   with a block string to work with. The string contains a nonce
   that is iteratively incremented until the string hashes to the
   correct structure (leading zeroes) or the thread runs out of
   attempts. */
void *
mining_thread (void * _params)
{
  struct mining_params *params = (struct mining_params *) _params;
  char *block = params->block;
  char *nonce_loc = block + strlen (block) - 16; 
  size_t count = 0;
  uint8_t hash[SHA_DIGEST_LENGTH];

  while (count < params->attempts)
    {
      /* Update nonce in the string and re-calculate the hash */
      snprintf (nonce_loc, 17, "%016" PRIx64, params->nonce++);
      memset (hash, 0, SHA_DIGEST_LENGTH);
      unsigned char *md =
        SHA1 ((unsigned char *)block, (unsigned long) params->blocklen, hash);

      /* Check to see if the hash begins with the desired number of
         leading zeroes */
      bool zeroes = true;
      int i = 0;
      for (i = 0; i < BLOCKCHAIN_ZEROES; i++)
        if (md[i] != 0) zeroes = false;

      /* If the leading zeroes match, update the block and exit */
      if (zeroes)
        {
          char *hash_str = hash_to_string (md);
          block[strlen (block)] = ' ';
          strncat (block, hash_str, SHA_DIGEST_LENGTH * 2);
          free (hash_str);
          params->success = true;
          pthread_exit (NULL);
        }
      count++;
    }
  pthread_exit (NULL);
}

/* Create a block to append to the end of the chain. Can be
   parallelized for improved performance, but blockchain
   calculations are designed to be slow. */
char *
append_block (char *data, char *previous, size_t num_threads)
{
  uint64_t index = (uint64_t) strtoul ((char *)previous, NULL, 16);

  char *prev_hash = calloc (SHA_DIGEST_LENGTH * 2 + 1, sizeof (char));
  strncpy (prev_hash,
           (char *)&previous[strlen ((char *)previous) - SHA_DIGEST_LENGTH * 2],
           SHA_DIGEST_LENGTH * 2);

  /* Last block was validated, now construct a new block */
  time_t now = time (NULL);
  size_t blocklen = 0;
  char *block =
    construct_block (index + 1, prev_hash, localtime (&now),
                     data, strlen (data), 0, &blocklen);
  free (prev_hash);

  /* Repeatedly implement a fork/join pattern until the search is
     successful. Each time through the while-loop, create a fixed
     number of threads that will perform a maximum number of
     calculations with different nonces. For instance, using
     1,000,000 attempts per thread, the first thread will use nonces
     0 through 999,999, the second 1,000,000 through 1,999,999, and
     so forth. Once all threads have run, check to see if any were
     successful. If not, create a new set of threads that continue
     with the next group of possible nonces. */

  size_t attempts_per_thread = 1000000;
  uint64_t nonce = 0;
  bool success = false;

  while (! success)
    {
      struct mining_params params[num_threads];
      pthread_t thread[num_threads];
      int i;
      for (i = 0; i < num_threads; i++)
        {
          /* Each thread needs its own copy of the block, so that
             it can change the nonce portion of the string. Note
             that we cannot just use strdup() here, because that
             would not allocate the additional bytes needed to
             append the hash for the successul search. */
          params[i].block =
            calloc (blocklen + 2 + 2 * SHA_DIGEST_LENGTH, sizeof (char));
          strncpy (params[i].block, block, blocklen);
          params[i].blocklen = blocklen;
          params[i].nonce = nonce;
          params[i].attempts = attempts_per_thread;
          params[i].success = false;
          pthread_create (&thread[i], NULL, mining_thread, &params[i]);

          /* The starting nonce for each thread is repeatedly
             incremented to ensure no two threads search the same
             set of values */
          nonce += attempts_per_thread;
        }

      /* For this group of threads, check to see if any were
         successful. If so, replace the generated block string with
         that thread's copy. */
      for (i = 0; i < num_threads; i++)
        {
          pthread_join (thread[i], NULL);
          if (params[i].success && !success)
            {
              free (block);
              block = params[i].block;
              success = true;
            }
          else
            free (params[i].block);
        }
    }

  return block;
}

/* Create a genesis block to serve as the foundation */
char *
create_genesis_block (void)
{
  /* Start off with a bogus 0 hash */
  uint8_t hash[SHA_DIGEST_LENGTH];
  memset (hash, 0, SHA_DIGEST_LENGTH);
  char *hash_str = hash_to_string (hash);

  /* Create a genesis block with the current time */
  time_t now = time (NULL);
  size_t blocklen = 0;
  char *genesis =
    construct_block (0, (char*) hash_str, localtime (&now),
                     "Genesis Block", 13, 0, &blocklen);
  free (hash_str);

  /* Compute the hash of the genesis block and append it */
  unsigned char *md =
    SHA1 ((unsigned char *)genesis, (unsigned long) blocklen, hash);
  hash_str = hash_to_string (md);
  genesis[strlen ((char *)genesis)] = ' ';
  strncat ((char *)genesis, hash_str, SHA_DIGEST_LENGTH * 2);
  free (hash_str);

  return genesis;
}

/* Constrct a block of the following form:

     index.previous hash.[timestamp].data.nonce

   (Dots denote spaces for readability of the line.)
   Note that the dynamic allocation also includes space for this
   block's hash at the end, but those bytes are initialized to
   all 0. */
char *
construct_block (uint64_t index, char *previous,
                 struct tm *timestamp, char *data, size_t length,
                 uint64_t nonce, size_t *block_length)
{
  *block_length =
    16 + 16 + length + 4 +  // index, nonce, length, spaces
    SHA_DIGEST_LENGTH * 2 + // previous hash
    ASCTIME_LENGTH + 2;     // [ASCII timestamp]

  /* Allocate extra space for the block's hash, as well */
  char *block =
    calloc (*block_length + 2 + SHA_DIGEST_LENGTH * 2, sizeof (char));

  /* Both the index and the nonce are padded with leading 0s to be
     fixed width */
  snprintf (block, 17, "%016" PRIx64, index);
  block[16] = ' ';
  strncat (block, previous, SHA_DIGEST_LENGTH * 2);

  /* For the [timestamp] the asctime returns a trailing
     '\n' that must be overwritten with the ']' */
  block[SHA_DIGEST_LENGTH * 2 + 17] = ' ';
  block[SHA_DIGEST_LENGTH * 2 + 18] = '[';
  strncat (block, asctime (timestamp), *block_length - strlen (block));
  block[strlen (block) - 1] = ']';

  /* For readability of data, add a space before and after */
  block[strlen (block)] = ' ';
  strncat (block, data, length);
  block[*block_length - 17] = ' ';

  snprintf (block + *block_length - 16, 17, "%016" PRIx64, nonce);

  return block;
}

/* Convert a SHA hash to a readable hex string */
char *
hash_to_string (uint8_t *hash)
{
  size_t j;
  char *str = calloc (SHA_DIGEST_LENGTH * 2 + 1, sizeof (char));

  /* Each byte gets split into two hex digits */
  for (j = 0; j < SHA_DIGEST_LENGTH; j++)
    sprintf (&str[j*2], "%02x", hash[j]);
  return str;
}
«  9.7. Consensus in Distributed Systems   ::   Contents   ::   10.1. C Language Reintroduction  »

Contact Us License