This is a basic, simple crypto system in which all nodes share a genesis address but each node has its own block chain. Thus, there is no consensus mechanism/protocol -- that must be handled by the application/network.
This system uses elliptic curves and sha256 to guarantee only three things:
- Every block within a chain was created by the address authorized in the genesis block.
- Every block will meet a customizable PoW difficulty threshold to avoid spam.
- Each genesis block will include that node's public key for ECDHE (encrypted communications).
As long as your protocol allows it, there can be any number of subnetworks using different genesis addresses. Trust within a network largely depends upon the secrecy of the genesis key and the trustworthiness of its owner. For embedded devices, you can accomplish this by including the genesis seed in a file that is deleted after generating the genesis block on the first run or by having the device make an API request during the first run (which should be on a secure network) supplying its node address, public key, and difficulty configuration and receiving a genesis block.
Block chains are saved/loaded with a rudimentary file IO system as this was originally designed for use in small devices. I recommend writing custom serializer/deserializer and save/load functions for prod applications.
There is no version control in the block headers. If necessary, add it to the body and implement version checking in the application. Or just fork the library.
Likewise, there are no timestamps in the block headers. This is because each node has its own block chain, and the system was envisioned for use with embedded devices. If necessary, add timestamps to the body or fork the library.
This uses the PyNaCL library for Ed25519 signatures and sha256 hashes. There are
methods for ECDH and ephemeral ECDH using Curve25519 for messaging between nodes,
hence the inclusion of Curve25519 public keys in genesis blocks. They are called
simply encrypt
, decrypt
, encrypt_sealed
, and decrypt_sealed
.
There are two reasons for using sha256 hashes instead of the sha512 signatures:
- When a block references another block on another block chain, it will be a total of 64 bytes: 32 for the address and 32 for the block hash. This saves some bandwidth in low-bandwidth networks for embedded devices, e.g. LoRa.
- It is easier on human eyes to use 256 bit hashes, and this scheme allows a theoretical maximum of 2^(256-difficulty) possible hashes per block chain/node, which is sufficient for any task.
There are two PoW difficulty algorithms available:
- The classic preceding null bytes; and
- Repeating end digits, through which Kek, the Egyptian god/goddess pair representing the primordial chaos/darkness that led to creation, supposedly communicates with their followers on 4chan.
They are mathematically identical given n difficulty: n null bytes restricts the set of valid hashes by (1/256)^n by restricting the first n digits to 1 value of the possible 256, while n+1 repeating digits takes the last digit and restricts the n preceding digits to be a copy of that digit, thus (1/256)^n.
- Install the
python3-nacl
library. - Put
blockchain.py
somewhere in the project files. from [path/to/blockchain] import BasicBlockChain
See sample.py
for some sample code.
Inherits from list and has these definitions:
-
Constructors
__init__
- from_seed
- from_chain
- from_genesis_key
- from_genesis_block
-
Non-constructor class methods
- verify_block
- verify_genesis_block
- verify_chain
-
Instance methods
- add_block
- encrypt
- decrypt
- encrypt_sealed
- decrypt_sealed
- sort (override)
-
Static methods
- meets_difficulty
- create_block
- create_genesis_block
Returns BasicBlockChain with following instance attributes:
difficulty
: 1difficulty_mode
: 0address
: empty byte stringpublic_key
: empty byte string
Parameter:
seed
: 32 bytes to seed the CSPRNG
Returns a BasicBlockChain with the following instance attributes:
seed
: bytessigning_key
:nacl.signing.SigningKey
verify_key
:nacl.signing.VerifyKey
address
: byte string of verify_keyprivate_key
:nacl.public.PrivateKey
public_key
:nacl.public.PublicKey
The SigningKey is derived from the seed, and all other values are derived from it.
Default values for difficulty
and difficulty_mode
can be overridden; this is
true for every class method that takes those parameters.
Parameter:
chain
: list
Returns a BasicBlockChain with the contents of chain
and the following instance attributes:
address
: byte stringpublic_key
:nacl.public.PublicKey
Parameter:
genesis_key
:nacl.signing.SigningKey
Generates a seed and returns result of BasicBlockChain.from_seed with a genesis block and genesis_address
attribute.
Parameter:
genesis_block
: dict of form:block_height
: inthash
: 32 byte stringsignature
: 64 byte stringaddress
: 32 byte string genesis addressnode_address
: 32 byte stringnonce
: 16 bytespublic_key
:nacl.public.PublicKey
Returns a BasicBlockChain with the genesis_block appended and the following instance attributes:
public_key
: nacl.public.PublicKeyaddress
: byte string of node address
Parameter:
data
: bytes
Calls create_block
and appends the result to self
. Returns None
.
Parameters:
signature
: byte string of result fromnacl.signing.SigningKey.sign()
difficulty=1
: int, minimum number of preceding zeroes in block hashdifficulty_mode=0
: int, which of two difficulty algorithms to use
Returns boolean:
True
if the sha256 of the signature has difficulty number of preceding zeroes or difficulty+1 repeating end digitsFalse
otherwise
(For brevity, I will omit explanation of difficulty=1, difficulty_mode=0 hereinafter.)
Parameters:
- signing_key:
nacl.signing.SigningKey
- previous_block: byte string or dict
- body: byte string
Returns dict of this form:
{
block_height: int,
hash: 32 bytes,
signature: 64 bytes,
address: 32 bytes,
previous_block: 32 bytes,
nonce: 16 bytes,
body: variable length byte string
}
@staticmethod create_genesis_block (genesis_key, node_address, public_key, difficulty=1, difficulty_mode=0)
Parameters:
genesis_key
:nacl.signing.SigningKey
node_address
:_key
element fromnacl.signing.VerifyKey
, e.g.node.verify_key._key
public_key
:_public_key
element fromnacl.public.PublicKey
, e.g.node.public_key._public_key
Returns a dict of this form:
{
block_height: 0,
hash: 32 bytes,
signature: 64 bytes,
address: 32 bytes (genesis_address),
node_address: 32 bytes,
nonce: 16 bytes,
public_key: 32 bytes
}
Parameters:
block
: dict (seecreate_block
above)
Returns a boolean:
False
if block hash does not meet difficulty levelFalse
if block signature fails verificationFalse
if the block is malformed/missing dataTrue
if all checks are passed
Parameters:
block
: dictgenesis_address
: byte string
Returns a boolean:
False
if block address is not the genesis_addressFalse
if block hash does not meet difficulty levelFalse
if block signature fails verificationFalse
if the block is malformed/missing dataTrue
if all checks are passed
Parameters:
blocks
: list of dictsgenesis_address
: byte string of genesis address
Returns a boolean:
False
ifverify_genesis_block
fails on first blockFalse
ifverify_block
fails on any other blockFalse
if any non-genesis block does not reference previous blockTrue
if all checks are passed
Parameters:
public_key
:nacl.public.PublicKey
plaintext
: bytes
Does ECDHE and returns the ciphertext.
Parameters:
public_key
:nacl.public.PublicKey
ciphertext
: bytes
Does ECDHE and returns the plaintext.
Parameters:
public_key
:nacl.public.PublicKey
plaintext
: bytes
Does ephemeral ECDHE and returns the ciphertext.
Parameter:
ciphertext
: bytes
Does ephemeral ECDHA and returns the plaintext.
Meant to be used from static context. Has these definitions:
-
Non-constructor class methods:
- save_block_chain
- find_block_hash
- load_block
- load_genesis_block
- load_block_chain
- unpack_block
- unpack_chain
-
Static methods:
- unpack_genesis_block
- block_index
- block_index_hex
- pack_block
- pack_genesis_block
- print_block
- print_block_chain
Parameters:
path
: string (should be namespaced with genesis_address if allowing subnetworks)name
: string (should be hex or b64 of node address)chain
: list of packed (bytes) or unpacked (dict) blocks
Saves blocks to flat files. Creates path/name directory if necessary. No return value.
Parameters:
path
: stringname
: stringheight
: int
Parses the (human-readable) index
file for the block chain and returns the hex hash of the requested block height.
Parameters:
path
: stringname
: stringhash
: string
Reads the block file and returns cls.unpack_block(block)
.
Parameters:
path
: stringname
: string
Reads the genesis file and returns cls.unpack_genesis_block(block)
.
Parameters:
path
: string (should be namespaced with genesis_address if allowing subnetworks)name
: string (should be hex or b64 of node address)
Loads a block chain from path/name
. Returns a BasicBlockChain
of unpacked blocks.
Parameter:
block_bytes
: byte string (at least 178 bytes long)
Returns dict of same form as create_block
.
Raises ValueError
if len(block_bytes) < 178.
Parameter:
- block_bytes: byte string (208 bytes exactly)
Returns dict of same form as create_genesis_block
.
Raises ValueError
if len(block_bytes) != 210.
Parameter:
chain
:list
of packed blocks (bytes)
Returns a list
of unpacked blocks (dicts of forms outlined above).
Parameter:
block
:dict
Encodes the block height in bytes.
Parameter:
block
:dict
Returns human-readable bytes
for the index file. Of form numeral_height:hex_hash
.
Parameter:
block
:dict
of form displayed above
Returns a byte string: index + hash + signature + address + previous_block + nonce + body
Parameter:
block
:dict
of form displayed above
Returns a byte string: \x01\x00 + hash + signature + genesis_address + node_address + nonce + public_key
Parameter:
block
:dict
Prints the block in clean, human-readable format.
Parameter:
blockchain
:BasicBlockChain
Prints the chain in clean, human-readable format. Does not print instance attributes.
- Write new serializer that uses SQLite.
- Add X3DH (Extended Triple Diffie-Hellman): signal.org/docs/specifications/x3dh/
- Add Double Ratchet for inter-node messaging: signal.org/docs/specifications/doubleratchet/
Copyright (c) 2019 Jonathan Voss
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.