Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[![CI](https://github.com/tinychainorg/tinychain/actions/workflows/go.yml/badge.svg)](https://github.com/tinychainorg/tinychain/actions/workflows/go.yml)

![](./explorer/assets/logo.png)

[Website](https://www.tinycha.in) | [API documentation](https://pkg.go.dev/github.com/tinychainorg/tinychain) | [Concepts](./docs/concepts.md)

**tinychain is the tiniest blockchain implementation in the world.**
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/wallet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package cmd
8 changes: 4 additions & 4 deletions core/bitset.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (

// A bit string is a fixed-length string of bits (0s and 1s) used to compactly represent a set of integers. Each bit at index `i` represents the membership of integer `i` in the set.
//
// For example, the bitstring 1010 represents the set {1, 3}.
// For example, the bitstring 0101 represents the set {1, 3}.
// The size of the string is 4 bits, and can represent a set of 4 integers.
// Bit strings become efficient to use when the number of integers is large.
// Bit sets become efficient to use when the count of integers is large.
// ie. when we have a set of 1000 integers, we can represent it with:
// - naively: 1000 * uint32 = 4000 bytes
// - naively as: 1000 * uint32 = 4000 bytes
// - with a bitstring: 1000 bits = 125 bytes
type Bitset []byte

Expand All @@ -23,7 +23,7 @@ func NewBitsetFromBuffer(buf []byte) *Bitset {
return (*Bitset)(&buf)
}

// Size returns the number of bits in the bitstring.
// Size returns the number of integers countable in the bit set.
func (b *Bitset) Size() int {
return len(*b) * 8
}
Expand Down
47 changes: 37 additions & 10 deletions core/nakamoto/blockdag.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ func (dag *BlockDAG) initialiseBlockDAG() error {

dag.log.Printf("Inserted genesis block hash=%s work=%s\n", hex.EncodeToString(genesisBlockHash[:]), work.String())

// Insert the genesis block transactions.
err = dag.IngestBlockBody(genesisBlock.Transactions)
if err != nil {
return err
}

return nil
}

Expand All @@ -163,10 +169,9 @@ func (dag *BlockDAG) updateHeadersTip() error {
if prev_tip.Hash != curr_tip.Hash {
dag.log.Printf("New headers tip: height=%d hash=%s\n", curr_tip.Height, curr_tip.HashStr())
dag.HeadersTip = curr_tip
if dag.OnNewHeadersTip == nil {
return nil
if dag.OnNewHeadersTip != nil {
dag.OnNewHeadersTip(curr_tip, prev_tip)
}
dag.OnNewHeadersTip(curr_tip, prev_tip)
}

return nil
Expand All @@ -182,10 +187,6 @@ func (dag *BlockDAG) updateFullTip() error {
if prev_tip.Hash != curr_tip.Hash {
dag.log.Printf("New full tip: height=%d hash=%s\n", curr_tip.Height, curr_tip.HashStr())
dag.FullTip = curr_tip
if dag.OnNewFullTip == nil {
return nil
}
dag.OnNewFullTip(curr_tip, prev_tip)
if dag.OnNewFullTip != nil {
dag.OnNewFullTip(curr_tip, prev_tip)
}
Expand All @@ -211,6 +212,22 @@ func (dag *BlockDAG) UpdateTip() error {
return nil
}

// Validation rules for blocks:
// 1. Verify parent is known.
// 2. Verify timestamp is within bounds.
// TODO: subjectivity.
// 3. Verify num transactions is the same as the length of the transactions list.
// 4a. Verify coinbase transcation is present.
// 4b. Verify transactions are valid.
// 5. Verify transaction merkle root is valid.
// 6. Verify POW solution is valid.
// 6a. Compute the current difficulty epoch.
// 6b. Verify POW solution.
// 6c. Verify parent total work is correct.
// 7. Verify block size is within bounds.
// 8. Ingest block into database store.
func (dag *BlockDAG) __doc() {}

// Ingests a block header, and recomputes the headers tip. Used by light clients / SPV sync.
func (dag *BlockDAG) IngestHeader(raw BlockHeader) error {
// 1. Verify parent is known.
Expand Down Expand Up @@ -346,7 +363,7 @@ func (dag *BlockDAG) IngestBlockBody(body []RawTransaction) error {
}
rows.Close()

// Verify we have not already ingested the txs.
// Verify we have not already ingested the txs for this block.
rows, err = dag.db.Query(`select count(*) from transactions_blocks where block_hash = ?`, blockhashBuf)
if err != nil {
return err
Expand Down Expand Up @@ -385,12 +402,17 @@ func (dag *BlockDAG) IngestBlockBody(body []RawTransaction) error {
}

// 4. Verify transactions are valid.
// 4a. Verify coinbase tx is present.
if len(raw.Transactions) < 1 {
return fmt.Errorf("Missing coinbase tx.")
}
// 4b. Verify transactions.
// TODO: We can parallelise this.
// This is one of the most expensive operations of the blockchain node.
for i, block_tx := range raw.Transactions {
dag.log.Printf("Verifying transaction %d\n", i)
isValid := core.VerifySignature(
hex.EncodeToString(block_tx.FromPubkey[:]),
block_tx.FromPubkey,
block_tx.Sig[:],
block_tx.Envelope(),
)
Expand Down Expand Up @@ -506,12 +528,17 @@ func (dag *BlockDAG) IngestBlock(raw RawBlock) error {
}

// 4. Verify transactions are valid.
// 4a. Verify coinbase tx is present.
if len(raw.Transactions) < 1 {
return fmt.Errorf("Missing coinbase tx.")
}
// 4b. Verify transactions.
// TODO: We can parallelise this.
// This is one of the most expensive operations of the blockchain node.
for i, block_tx := range raw.Transactions {
dag.log.Printf("Verifying transaction %d\n", i)
isValid := core.VerifySignature(
hex.EncodeToString(block_tx.FromPubkey[:]),
block_tx.FromPubkey,
block_tx.Sig[:],
block_tx.Envelope(),
)
Expand Down
24 changes: 13 additions & 11 deletions core/nakamoto/blockdag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func newValidTx(t *testing.T) (RawTransaction, error) {
copy(tx.Sig[:], sig)

// Sanity check verify.
if !core.VerifySignature(wallets[0].PubkeyStr(), sig, envelope) {
if !core.VerifySignature(wallets[0].PubkeyBytes(), sig, envelope) {
t.Fatalf("Failed to verify signature.")
}

Expand Down Expand Up @@ -404,19 +404,20 @@ func TestDagGetBlockByHashGenesis(t *testing.T) {
t.Logf("Genesis block size: %d\n", block.SizeBytes)

// RawBlock.
// find:GENESIS-BLOCK-ASSERTS
genesisNonce := Bytes32ToBigInt(genesisBlock.Nonce)
assert.Equal(conf.GenesisParentBlockHash, block.ParentHash)
assert.Equal(uint64(0), block.Timestamp)
assert.Equal(uint64(0), block.NumTransactions)
assert.Equal([32]byte{}, block.TransactionsMerkleRoot)
assert.Equal(big.NewInt(21).String(), genesisNonce.String())
assert.Equal(uint64(1), block.NumTransactions)
assert.Equal([32]uint8{0x59, 0xe0, 0xaa, 0xf, 0x1f, 0xe6, 0x6f, 0x3b, 0xe, 0xb0, 0xc, 0xa3, 0x31, 0x33, 0x1a, 0x69, 0x1, 0xc4, 0xc4, 0xa1, 0x21, 0x99, 0xba, 0xa0, 0x16, 0x77, 0xfd, 0xe2, 0xd4, 0xb7, 0xc6, 0x88}, block.TransactionsMerkleRoot)
assert.Equal(big.NewInt(19).String(), genesisNonce.String())
// Block.
assert.Equal(uint64(0), block.Height)
assert.Equal(GetIdForEpoch(genesisBlock.Hash(), 0), block.Epoch)
assert.Equal(uint64(208), block.SizeBytes)
assert.Equal(HexStringToBytes32("0877dbb50dc6df9056f4caf55f698d5451a38015f8e536e9c82ca3f5265c38c7"), block.Hash)
assert.Equal(uint64(0x1ab), block.SizeBytes)
assert.Equal(HexStringToBytes32("04ce8ce628e56bab073ff2298f1f9d0e96d31fb7a81f388d8fe6e4aa4dc1aaa8"), block.Hash)
t.Logf("Block: acc_work=%s\n", block.AccumulatedWork.String())
assert.Equal(big.NewInt(30).String(), block.AccumulatedWork.String())
assert.Equal(big.NewInt(53).String(), block.AccumulatedWork.String())
}

func TestDagBlockDAGInitialised(t *testing.T) {
Expand Down Expand Up @@ -489,14 +490,15 @@ func TestDagBlockDAGInitialised(t *testing.T) {
t.Logf("Block: %v\n", block.Hash)

// Check the genesis block.
// find:GENESIS-BLOCK-ASSERTS
genesisNonce := Bytes32ToBigInt(genesisBlock.Nonce)
assert.Equal(genesisBlock.Hash(), block.Hash)
assert.Equal(conf.GenesisParentBlockHash, block.ParentHash)
assert.Equal(big.NewInt(0).String(), block.ParentTotalWork.String())
assert.Equal(uint64(0), block.Timestamp)
assert.Equal(uint64(0), block.NumTransactions)
assert.Equal([32]byte{}, block.TransactionsMerkleRoot)
assert.Equal(big.NewInt(21).String(), genesisNonce.String())
assert.Equal(uint64(1), block.NumTransactions)
assert.Equal([32]uint8{0x59, 0xe0, 0xaa, 0xf, 0x1f, 0xe6, 0x6f, 0x3b, 0xe, 0xb0, 0xc, 0xa3, 0x31, 0x33, 0x1a, 0x69, 0x1, 0xc4, 0xc4, 0xa1, 0x21, 0x99, 0xba, 0xa0, 0x16, 0x77, 0xfd, 0xe2, 0xd4, 0xb7, 0xc6, 0x88}, block.TransactionsMerkleRoot)
assert.Equal(big.NewInt(19).String(), genesisNonce.String())
assert.Equal(uint64(0), block.Height)
assert.Equal(GetIdForEpoch(genesisBlock.Hash(), 0), block.Epoch)

Expand All @@ -523,7 +525,7 @@ func TestDagBlockDAGInitialised(t *testing.T) {
// Check the genesis epoch.
t.Logf("Genesis epoch: %v\n", epoch.Id)
assert.Equal(GetIdForEpoch(genesisBlock.Hash(), 0), epoch.Id)
assert.Equal(HexStringToBytes32("0877dbb50dc6df9056f4caf55f698d5451a38015f8e536e9c82ca3f5265c38c7"), epoch.StartBlockHash)
assert.Equal(HexStringToBytes32("04ce8ce628e56bab073ff2298f1f9d0e96d31fb7a81f388d8fe6e4aa4dc1aaa8"), epoch.StartBlockHash)
assert.Equal(uint64(0), epoch.StartTime)
assert.Equal(uint64(0), epoch.StartHeight)
assert.Equal(conf.GenesisDifficulty, epoch.Difficulty)
Expand Down
42 changes: 18 additions & 24 deletions core/nakamoto/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,33 @@ type ConsensusConfig struct {
}

// Builds the raw genesis block from the consensus configuration.
//
// NOTE: This function essentially creates the genesis block from a short configuration.
// If the values are changed, the genesis hash will change, and a bunch of tests will fail / need to be updated with the new hash.
// These tests have been marked with the comment string find:GENESIS-BLOCK-ASSERTS so you can find them easily.
func GetRawGenesisBlockFromConfig(consensus ConsensusConfig) RawBlock {
txs := []RawTransaction{
RawTransaction{
Version: 1,
Sig: [64]byte{0x86, 0xaf, 0x5f, 0x4b, 0x76, 0xea, 0x1c, 0xd2, 0xfb, 0xd4, 0x0f, 0xec, 0x93, 0x90, 0x70, 0x58, 0x47, 0xa1, 0x36, 0xb2, 0xc7, 0x0d, 0x10, 0x7b, 0xdd, 0x3e, 0x92, 0x27, 0xfd, 0xcb, 0x5e, 0xbb, 0x1c, 0x50, 0x0e, 0xfa, 0x02, 0x6a, 0x30, 0x44, 0x71, 0x15, 0xcc, 0x97, 0xf4, 0x15, 0x7f, 0x56, 0xd3, 0x3d, 0xb3, 0x30, 0xd6, 0x66, 0x06, 0xbb, 0xc1, 0x02, 0xae, 0x41, 0x39, 0xdb, 0x67, 0x93},
FromPubkey: [65]byte{0x04, 0x61, 0xbf, 0x49, 0x39, 0x38, 0x55, 0xec, 0x77, 0x08, 0x1b, 0x61, 0xe1, 0xb1, 0x5d, 0x6a, 0xd9, 0x2b, 0x14, 0x26, 0x81, 0xe4, 0x0c, 0xeb, 0x07, 0x33, 0x4b, 0x63, 0x32, 0x73, 0x40, 0x2e, 0x24, 0xb2, 0x71, 0xc9, 0x14, 0x90, 0xc6, 0x39, 0x77, 0x5d, 0x0f, 0x00, 0x75, 0x9a, 0xc6, 0x1a, 0xf3, 0x5a, 0x4b, 0x24, 0xc6, 0x74, 0xf2, 0x81, 0x0c, 0xc1, 0x29, 0xfa, 0x04, 0x43, 0x6a, 0xa6, 0x84},
ToPubkey: [65]byte{0x04, 0x61, 0xbf, 0x49, 0x39, 0x38, 0x55, 0xec, 0x77, 0x08, 0x1b, 0x61, 0xe1, 0xb1, 0x5d, 0x6a, 0xd9, 0x2b, 0x14, 0x26, 0x81, 0xe4, 0x0c, 0xeb, 0x07, 0x33, 0x4b, 0x63, 0x32, 0x73, 0x40, 0x2e, 0x24, 0xb2, 0x71, 0xc9, 0x14, 0x90, 0xc6, 0x39, 0x77, 0x5d, 0x0f, 0x00, 0x75, 0x9a, 0xc6, 0x1a, 0xf3, 0x5a, 0x4b, 0x24, 0xc6, 0x74, 0xf2, 0x81, 0x0c, 0xc1, 0x29, 0xfa, 0x04, 0x43, 0x6a, 0xa6, 0x84},
Amount: 5000000000,
Fee: 0,
Nonce: 0,
},
}
block := RawBlock{
// Special case: The genesis block has a parent we don't know the preimage for.
ParentHash: consensus.GenesisParentBlockHash,
ParentTotalWork: [32]byte{},
Difficulty: BigIntToBytes32(consensus.GenesisDifficulty),
Timestamp: 0,
NumTransactions: 0,
TransactionsMerkleRoot: [32]byte{},
NumTransactions: 1,
TransactionsMerkleRoot: GetMerkleRootForTxs(txs),
Nonce: [32]byte{},
Graffiti: [32]byte{0xca, 0xfe, 0xba, 0xbe, 0xde, 0xca, 0xfb, 0xad, 0xde, 0xad, 0xbe, 0xef}, // 0x cafebabe decafbad deadbeef
Transactions: []RawTransaction{},
Transactions: txs,
}

// Mine the block.
Expand All @@ -55,26 +70,5 @@ func GetRawGenesisBlockFromConfig(consensus ConsensusConfig) RawBlock {

fmt.Printf("Genesis block hash=%x work=%s\n", block.Hash(), work.String())

// to block header
// header := BlockHeader{
// ParentHash: block.ParentHash,
// ParentTotalWork: block.ParentTotalWork,
// Difficulty: block.Difficulty,
// Timestamp: block.Timestamp,
// NumTransactions: block.NumTransactions,
// TransactionsMerkleRoot: block.TransactionsMerkleRoot,
// Nonce: block.Nonce,
// Graffiti: block.Graffiti,
// }
// fmt.Printf("Genesis header block hash=%x\n", header.BlockHash())
// fmt.Printf("ParentHash: %x\n", header.ParentHash)
// fmt.Printf("ParentTotalWork: %x\n", header.ParentTotalWork)
// fmt.Printf("Difficulty: %x\n", header.Difficulty)
// fmt.Printf("Timestamp: %x\n", header.Timestamp)
// fmt.Printf("NumTransactions: %d\n", header.NumTransactions)
// fmt.Printf("TransactionsMerkleRoot: %x\n", header.TransactionsMerkleRoot)
// fmt.Printf("Nonce: %x\n", header.Nonce)
// fmt.Printf("Graffiti: %x\n", header.Graffiti)

return block
}
60 changes: 50 additions & 10 deletions core/nakamoto/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math/big"
"testing"

"github.com/liamzebedee/tinychain-go/core"
"github.com/stretchr/testify/assert"
)

Expand All @@ -24,18 +25,57 @@ func TestGetRawGenesisBlockFromConfig(t *testing.T) {
}

// Get the genesis block.
genesisBlock := GetRawGenesisBlockFromConfig(conf)
genesisNonce := Bytes32ToBigInt(genesisBlock.Nonce)
block := GetRawGenesisBlockFromConfig(conf)
genesisNonce := Bytes32ToBigInt(block.Nonce)

// Print the hash.
fmt.Printf("Genesis block hash: %x\n", genesisBlock.Hash())
fmt.Printf("Genesis block hash: %x\n", block.Hash())

// Check the genesis block.
assert.Equal(HexStringToBytes32("0877dbb50dc6df9056f4caf55f698d5451a38015f8e536e9c82ca3f5265c38c7"), genesisBlock.Hash())
assert.Equal(conf.GenesisParentBlockHash, genesisBlock.ParentHash)
assert.Equal(BigIntToBytes32(*big.NewInt(0)), genesisBlock.ParentTotalWork)
assert.Equal(uint64(0), genesisBlock.Timestamp)
assert.Equal(uint64(0), genesisBlock.NumTransactions)
assert.Equal([32]byte{}, genesisBlock.TransactionsMerkleRoot)
assert.Equal(big.NewInt(21).String(), genesisNonce.String())
// find:GENESIS-BLOCK-ASSERTS
assert.Equal(HexStringToBytes32("04ce8ce628e56bab073ff2298f1f9d0e96d31fb7a81f388d8fe6e4aa4dc1aaa8"), block.Hash())
assert.Equal(conf.GenesisParentBlockHash, block.ParentHash)
assert.Equal(BigIntToBytes32(*big.NewInt(0)), block.ParentTotalWork)
assert.Equal(uint64(0), block.Timestamp)
assert.Equal(uint64(1), block.NumTransactions)
assert.Equal([32]uint8{0x59, 0xe0, 0xaa, 0xf, 0x1f, 0xe6, 0x6f, 0x3b, 0xe, 0xb0, 0xc, 0xa3, 0x31, 0x33, 0x1a, 0x69, 0x1, 0xc4, 0xc4, 0xa1, 0x21, 0x99, 0xba, 0xa0, 0x16, 0x77, 0xfd, 0xe2, 0xd4, 0xb7, 0xc6, 0x88}, block.TransactionsMerkleRoot)
assert.Equal(big.NewInt(19).String(), genesisNonce.String())
}

func formatByteArrayDynamic(b []byte) string {
out := fmt.Sprintf("[%d]byte{", len(b))
for i, v := range b {
if i > 0 {
out += ", "
}
out += fmt.Sprintf("0x%02x", v)
}
out += "}"
return out
}

func TestWalletCreateSignTransferTx(t *testing.T) {
wallet, err := core.CreateRandomWallet()
if err != nil {
panic(err)
}
tx := MakeCoinbaseTx(wallet, GetBlockReward(0))

// JSON dump.
// str, err := json.Marshal(tx)
// if err != nil {
// panic(err)
// }

// Print as a Go-formatted RawTransaction{} for usage in genesis.go.
fmt.Printf("Coinbase tx:\n")
fmt.Printf("RawTransaction {\n")
fmt.Printf("Version: %d,\n", tx.Version)
fmt.Printf("Sig: %s,\n", formatByteArrayDynamic(tx.Sig[:]))
fmt.Printf("FromPubkey: %s,\n", formatByteArrayDynamic(tx.FromPubkey[:]))
fmt.Printf("ToPubkey: %s,\n", formatByteArrayDynamic(tx.ToPubkey[:]))
fmt.Printf("Amount: %d,\n", tx.Amount)
fmt.Printf("Fee: %d,\n", tx.Fee)
fmt.Printf("Nonce: %d,\n", tx.Nonce)
fmt.Printf("}\n")
}
11 changes: 6 additions & 5 deletions core/nakamoto/miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ func NewMiner(dag BlockDAG, coinbaseWallet *core.Wallet) *Miner {
}
}

func MakeCoinbaseTx(wallet *core.Wallet) RawTransaction {
func MakeCoinbaseTx(wallet *core.Wallet, amount uint64) RawTransaction {
// Construct coinbase tx.
tx := RawTransaction{
Version: 1,
Sig: [64]byte{},
FromPubkey: wallet.PubkeyBytes(),
ToPubkey: wallet.PubkeyBytes(),
Amount: 50,
Amount: amount,
Fee: 0,
Nonce: 0,
}
Expand Down Expand Up @@ -148,9 +148,6 @@ func (miner *Miner) MineWithStatus(hashrateChannel chan float64, solutionChannel

// Creates a new block template for mining.
func (miner *Miner) MakeNewPuzzle() POWPuzzle {
// Construct coinbase tx.
coinbaseTx := MakeCoinbaseTx(miner.CoinbaseWallet)

// Get the current tip.
current_tip, err := miner.dag.GetLatestFullTip()
if err != nil {
Expand All @@ -161,6 +158,10 @@ func (miner *Miner) MakeNewPuzzle() POWPuzzle {
current_tip = miner.GetTipForMining()
}

// Construct coinbase tx.
blockReward := GetBlockReward(int(current_tip.Height))
coinbaseTx := MakeCoinbaseTx(miner.CoinbaseWallet, blockReward)

// Get the block body.
blockBody := []RawTransaction{}
blockBody = append(blockBody, coinbaseTx)
Expand Down
2 changes: 1 addition & 1 deletion core/nakamoto/pow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func TestECDSASignatureVerifyTiming(t *testing.T) {
t.Fatalf("Failed to sign message: %s", err)
}

pubkey := wallet.PubkeyStr()
pubkey := wallet.PubkeyBytes()

// Measure start time.
start := time.Now()
Expand Down
Loading
Loading