7
\$\begingroup\$

There is a blockchain class with minimal functionality inspired by naivechain. It has template data and hash function parameters:

#include "blockchain_block.hpp"

#ifndef BLOCKCHAIN_HPP
#define BLOCKCHAIN_HPP

#include <string>
#include <list>
#include <type_traits>

namespace rzd {

template <
  typename T,
  typename Hash = std::hash<std::string>
> class blockchain {

 public:

  struct block;

  using hash_type = std::invoke_result_t<Hash, std::string>;
  using value_type = block;
  using data_type = T;
  using size_type = std::size_t;
  using difference_type = std::ptrdiff_t;
  using pointer = block*;
  using iterator = typename std::list<block>::iterator;
  using const_iterator = typename std::list<block>::const_iterator;

 private:

  std::list<block> chain;

  template <typename It>
  size_type valid_size(It first, It last) {
    if (*first != *begin() || first == last) {
      return {};
    }
    iterator pos{ std::next(begin()) };
    difference_type index{ 1 };
    for (auto it{ std::next(first) }; it != last; ++it) {
      if (it->valid(*std::prev(it))) {
        if (*pos == *it) {
          ++pos;
          ++index;
        }
      } else {
        return {};
      }
    }
    return index;
  }


 public:

  template <typename It>
  iterator replace(It first, It last) {
    if (auto index{ valid_size(first, last) },
        range{ std::distance(first, last) };
        index != 0 && range > size()) {
      chain.resize(range);
      return std::copy(std::next(first, index), last,
                      std::next(begin(), index));
    }
    return begin();
  }

  inline void push(const value_type& node) {
    if (node.valid(*std::prev(end())))
      chain.push_back({ node });
  }

  inline iterator begin() {
    return std::begin(chain);
  }

  inline const_iterator cbegin() const {
    return std::cbegin(chain);
  }

  inline iterator end() {
    return std::end(chain);
  }

  inline const_iterator cend() const {
    return std::cend(chain);
  }

  inline size_type size() const {
    return std::size(chain);
  }

  inline bool operator<(const blockchain& another) {
    return std::size(chain) < std::size(another);
  }

  inline bool operator>(const blockchain& another) {
    return std::size(chain) > std::size(another);
  }

  explicit blockchain(const data_type& data)
      : chain{ { { {}, data, {} } } } {}

};

}

#endif

And a block actually:

#ifndef BLOCKCHAIN_BLOCK_HPP
#define BLOCKCHAIN_BLOCK_HPP

#include "blockchain.hpp"
#include <iostream>
#include <sstream>
#include <chrono>

namespace rzd {

template <
  typename T,
  typename Hash
> struct blockchain<T, Hash>::block {

  using hash_type = typename blockchain::hash_type;
  using data_type = typename blockchain::data_type;
  using index_type = typename blockchain::size_type;
  using hasher = Hash;
  using time_type = std::chrono::time_point<std::chrono::system_clock>;
  using clock = std::chrono::system_clock;

  hasher hash_string;
  index_type index;
  data_type data;
  hash_type previous_hash;
  hash_type hash;
  time_type timestamp;

  hash_type get_hash() const {
    std::stringstream ss;
    ss << index
      << data
      << previous_hash
      << clock::to_time_t(timestamp);
    return hash_string(ss.str());
  }

  bool valid(const block& previous_block) const {
    if (index != previous_block.index + 1) {
      std::cerr << "Index "
                << index
                << " does not follow by "
                << previous_block.index
                << std::endl;
      return false;
    }
    if (previous_hash != previous_block.hash) {
      std::cerr << "Hash "
                << previous_hash
                << " is not equal to "
                << previous_block.hash
                << std::endl;
      return false;
    }
    if (hash != get_hash()) {
      std::cerr << "Hash "
                << hash
                << " is not equal to "
                << get_hash()
                << std::endl;
      return false;
    }
    return true;
  }

  bool operator==(const block& node) {
    return hash == node.hash;
  }

  bool operator!=(const block& node) {
    return hash != node.hash;
  }

  block(const index_type index,
        const data_type& data,
        const hash_type& previous_hash)
      : index{ index }
      , data{ data }
      , previous_hash{ previous_hash } {
    timestamp = clock::now();
    hash = get_hash();
  }

  block(block& previous_block, const data_type& data)
      : block(previous_block.index + 1,
              data,
              previous_block.hash) {}

  block(const block& block) = default;

  block() = default;

};

}

#endif

I also wrote some tests for that stuff:

#include "blockchain.hpp"
#include <string>
#include <list>
#include <cassert>

int main() {
  rzd::blockchain<std::string> chain{ "foo" };
  assert(chain.begin()->data == "foo");
  chain.push({ *std::prev(chain.end()), "bar" });
  assert(std::next(chain.begin())->data  == "bar");
  assert(std::size(chain) == 2);
  assert(std::next(chain.begin())->valid(*chain.begin()));
  assert(std::prev(chain.end())->previous_hash == chain.begin()->hash);
  chain.push({ *chain.begin(), "spam" });
  assert(std::size(chain) == 2);
  std::list<rzd::blockchain<std::string>::block> copy;
  copy.push_back(*chain.begin());
  copy.push_back(*std::next(chain.begin()));
  copy.push_back({ *std::prev(copy.end()), "eggs" });
  copy.push_back({ *std::prev(copy.end()), "sause" });
  assert(std::prev(copy.end())->valid(*std::prev(copy.end(), 2)));
  chain.replace(copy.begin(), copy.end());
  assert(std::size(chain) == 4);
  assert(std::prev(chain.end())->data == "sause");
  rzd::blockchain<std::string> chain2{ "toe" };
  assert(chain > chain2);
}

Does it correspond to blockchain principles?

Is there enough methods to call them from Python, for example?

Are there any refactoring possibilities?

\$\endgroup\$
3
  • \$\begingroup\$ IIRC templates are not binding friendly. Binding might require instantiating it manually. \$\endgroup\$ Apr 17, 2018 at 13:30
  • \$\begingroup\$ Is there a standard block chain API you are implementing? If so can you provide a link. \$\endgroup\$ Apr 17, 2018 at 15:07
  • \$\begingroup\$ @MartinYork I'm implementing something like this github.com/lhartikk/naivechain \$\endgroup\$
    – lisovskey
    Apr 17, 2018 at 15:46

1 Answer 1

1
\$\begingroup\$

I got rid of a bunch of Effective C++ warnings by providing initializers for members of block:

hasher hash_string = {};
index_type index = {};
data_type data = {};
hash_type previous_hash = {};
hash_type hash = {};
time_type timestamp = {};

I also eliminated a signed/unsigned comparison by making both index and range the same type here:

    if (size_type index{valid_size(first, last)},
                  range(std::distance(first, last));
            index != 0 && range > size()) {

It's unconventional to put constructors at the end of a class definition - most of us expect to see constructors, destructors and copy/move assignment at the beginning, right after the data members. That helps with understanding the basic behaviour and ownership model of the code.


This loop looks like it's a std::adjacent_find():

for (auto it{ std::next(first) };  it != last;  ++it) {
  if (it->valid(*std::prev(it))) {

Could the standard algorithm be used instead, for clearer code?


The const overloads of begin() and end() members are missing from blockchain:

inline iterator begin() const {
    return cbegin();
}

inline iterator end() const {
    return cend();
}

In blockchain, the comparison operators < and > ought to be const. Since C++20, we can implement spaceship operator instead:

std::partial_ordering operator<=>(const blockchain& another)
{ return size() <=> another.size(); }

And in block, == and != should be const; != can be defaulted:

bool operator==(const block& node) const {
    return hash == node.hash;
}

// bool operator!=(const block& node) const = default;

Why does block::valid() write to the standard output stream? That looks like leftover debugging, which should probably go to std::cerr or perhaps std::clog, but preferably neither - provide an interface for code to find more details if it needs them, but don't produce output if not requested.


using time_type = std::chrono::time_point<std::chrono::system_clock>;
using clock = std::chrono::system_clock;

I would reorder those, to give a automatically consistent pair of definitions:

using clock = std::chrono::system_clock;
using time_type = std::chrono::time_point<clock>;

Give consideration to making the clock type a template parameter, with sensible default. That can help enable high-precision timestamping, for example, or repeatable unit tests using a mock object.

template <
  typename T,
  typename Hash,
  typename Clock = std::chrono::system_clock
>

Similarly, I'd define the container type we use in a single place in blockchain:

using list_type = std::list<block>;
using iterator = list_type::iterator;
using const_iterator = list_type::const_iterator;
\$\endgroup\$
2
  • \$\begingroup\$ If operator==() compares hash, then operator</>/<=>() comparing size() seems like a bug. \$\endgroup\$
    – G. Sliepen
    Feb 21, 2022 at 7:30
  • 1
    \$\begingroup\$ Those operators apply to different types. No comment on whether comparing by size is meaningful, though. I've just edited to change the return type of <=>, since equal size doesn't imply equal chains. \$\endgroup\$ Feb 21, 2022 at 7:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.