Block structure
We are creating a directory named core and implementing a core package with functionalities of Blocks and Transactions.
We'll add Transaction, Hash and Encoding and Decoding of Blocks and Transaction with Hash.
mkdir core
core/block.go
package core
import (
"ProjectX/types"
"bytes"
"crypto/sha256"
"encoding/binary"
"io"
)
type Header struct {
Version uint32
PrevBlock types.Hash
Timestamp int64
Height uint32
Nonce uint64
}
type Block struct {
Header
Transactions []Transaction
hash types.Hash
}
Types and Hash
We will be doing every hash related work in package types, so creating a directory named types and implementing basic hash type and methods
mkdir types
Hash
type is array of 32 bytes uint8 as it contains 32 hexadecimal
Functions:
HashFromBytes
: generates a hash from a byte slice while enforcing a restriction that the input slice must be exactly 32 bytes long.RandomBytes
: generates a slice of random bytes with a length equal to the input size provided.RandomHash
: generates hash with length of 32 by utilizingRandomBytes
function
types/hash.go
package types
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
type Hash [32]uint8
func HashFromBytes(b []byte) Hash{
if len(b)!=32 {
msg := fmt.Sprintf("given bytes with length %d is should be 32",len(b))
panic(msg)
}
var value [32]uint8
for i:=0 ; i<len(b) ; i++ {
value[i] = b[i]
}
return value
}
func RandomBytes(size uint) []byte {
token := make([]byte, size)
rand.Read(token)
return token
}
func RandomHash() Hash{
return HashFromBytes(RandomBytes(32))
}
Encoding and decoding block header
EncodeBinary
: method encodes the Header
struct fields into binary data and writes them into the provided io.Writer
DecodeBinary
: method reads and decodes binary data from the provided io.Reader
into the Header
struct fields.
core/block.go
type Header struct {
Version uint32
PrevBlock types.Hash
Timestamp int64
Height uint32
Nonce uint64
}
func (h *Header)EncodeBinary(w io.Writer) error {
if err := binary.Write(w, binary.LittleEndian, &h.Version); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, &h.PrevBlock); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, &h.Timestamp); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, &h.Height); err != nil {
return err
}
return binary.Write(w, binary.LittleEndian, &h.Nonce);
}
func (h *Header)DecodeBinary(r io.Reader) error {
if err := binary.Read(r, binary.LittleEndian, &h.Version); err != nil {
return err
}
if err := binary.Read(r, binary.LittleEndian, &h.PrevBlock); err != nil {
return err
}
if err := binary.Read(r, binary.LittleEndian, &h.Timestamp); err != nil {
return err
}
if err := binary.Read(r, binary.LittleEndian, &h.Height); err != nil {
return err
}
return binary.Read(r, binary.LittleEndian, &h.Nonce);
}
Testing of Encoding and Decoding
The TestHeader_Encode_Decode
test ensures that the encoding and subsequent decoding operations of a Header
struct using binary I/O functions are successful and result in an accurate reproduction of the original Header
.
core/block_test.go
package core
import (
"ProjectX/types"
"bytes"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestHeader_Encode_Decode(t *testing.T){
h := &Header{
Version:1,
PrevBlock: types.RandomHash(),
Timestamp: time.Now().UnixNano(),
Height:32,
Nonce:12450,
}
buf := &bytes.Buffer{}
assert.Nil(t, h.EncodeBinary(buf))
hDecode := &Header{}
assert.Nil(t, hDecode.DecodeBinary(buf))
assert.Equal(t, hDecode, h)
}
make test
Transactions
We are only implementing Transaction
, so we can use it in our block
and do further encoding and decoding. It is not going to be implemented full-fledged.
We will work on Transaction
later on and will update it soon.
core/transaction.go
package core
import "io"
type Transaction struct {
}
func (t *Transaction) EncodeBinary(w io.Writer) error {
return nil
}
func (t *Transaction) DecodeBinary(r io.Reader) error {
return nil
}
Encoding and Decoding of block
EncodeBinary
method**:** encodes a Block
struct and its transactions into binary data by utilizing the EncodeBinary
method from the Header
and Transaction
structs. It writes this encoded data into the provided io.Writer
. If any error occurs during encoding, it returns the error immediately.
DecodeBinary
method**:** decodes binary data from the provided io.Reader
into the Block
struct and its associated transactions. It uses the DecodeBinary
method from the Header
and Transaction
structs to decode the data. If an error occurs during decoding, it returns the error.
core/block.go
func (b *Block)EncodeBinary(w io.Writer) error {
if err := b.Header.EncodeBinary(w); err != nil {
return err
}
for _, tx := range b.Transactions{
if err := tx.EncodeBinary(w); err != nil {
return err
}
}
return nil
}
func (b *Block)DecodeBinary(r io.Reader) error {
if err := b.Header.DecodeBinary(r); err != nil {
return err
}
for _, tx := range b.Transactions{
if err := tx.DecodeBinary(r); err != nil {
return err
}
}
return nil
}
Test it!!
This test verifies whether the encoding and decoding methods for the Block
works correctly.
func TestBlock_Encode_Decode(t *testing.T){
b := &Block{
Header: Header{
Version:1,
PrevBlock: types.RandomHash(),
Timestamp: time.Now().UnixNano(),
Height:32,
Nonce:12450,
},
Transactions: nil,
}
buf := &bytes.Buffer{}
assert.Nil(t, b.EncodeBinary(buf))
bDecode := &Block{}
assert.Nil(t, bDecode.DecodeBinary(buf))
assert.Equal(t, bDecode, b)
}
make test
It is working indeed!!
Creating block hash
We are implementing necessary functions which will help later.
ToSlice()
: Converts aHash
array into a byte slice by iterating over its elements and copying them into the slice.String()
: Converts theHash
array to a hexadecimal string by first converting it to a byte slice and then encoding it into a hexadecimal representation usinghex.EncodeToString
.IsZero()
: Checks if theHash
array is zero-valued by iterating over its elements and returningtrue
if all elements are zero; otherwise, it returnsfalse
.
types/hash.go
type Hash [32]uint8
func (h Hash) ToSlice() []byte {
b := make([]byte, 32)
for i:=0;i<32;i++ {
b[i] = h[i]
}
return b
}
func (h Hash) String() string {
return hex.EncodeToString(h.ToSlice())
}
func (h Hash) IsZero() bool {
for i := 0; i < len(h); i++{
if h[i] != 0 {
return false
}
}
return true
}
Hash()
: Generates and returns the hash value of the block by encoding the block's header into binary data, computing its SHA-256 hash if the block's existing hash value is zero, and finally returning the computed hash.
core/block.go
func (b *Block) Hash() types.Hash {
buf := &bytes.Buffer{}
b.Header.EncodeBinary(buf)
if b.hash.IsZero() {
b.hash = types.Hash(sha256.Sum256(buf.Bytes()))
}
return b.hash
}
Testing it then,
TestBlockHash()
: A test function that creates a sample Block
, generates its hash using the Hash()
method, asserts that the generated hash is not zero, and then prints the details of the hash for verification purposes.
core/block_test.go
func TestBlockHash(t *testing.T){
b := &Block{
Header: Header{
Version:1,
PrevBlock: types.RandomHash(),
Timestamp: time.Now().UnixNano(),
Height:32,
Nonce:12450,
},
Transactions: []Transaction{},
}
h := b.Hash()
assert.False(t, h.IsZero())
fmt.Printf("%+v",h)
}
make test
It's generating block hash!!✨
The following blog post will explore the code related to Crypto keypairs and signatures.✨
In this blog series, I'll be sharing code snippets related to blockchain architecture. While the code will be available on my GitHub, I want to highlight that the entire architecture isn't solely my own. I'm learning as I go, drawing inspiration and knowledge from various sources, including a helpful YouTube playlist that has contributed to my learning process.