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?
-
\$\begingroup\$ IIRC templates are not binding friendly. Binding might require instantiating it manually. \$\endgroup\$Incomputable– Incomputable2018年04月17日 13:30:05 +00:00Commented 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\$Loki Astari– Loki Astari2018年04月17日 15:07:21 +00:00Commented Apr 17, 2018 at 15:07
-
\$\begingroup\$ @MartinYork I'm implementing something like this github.com/lhartikk/naivechain \$\endgroup\$lisovskey– lisovskey2018年04月17日 15:46:12 +00:00Commented Apr 17, 2018 at 15:46
1 Answer 1
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;
-
\$\begingroup\$ If
operator==()
compareshash
, thenoperator<
/>
/<=>()
comparingsize()
seems like a bug. \$\endgroup\$G. Sliepen– G. Sliepen2022年02月21日 07:30:44 +00:00Commented 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\$Toby Speight– Toby Speight2022年02月21日 07:36:22 +00:00Commented Feb 21, 2022 at 7:36