diff --git a/README.md b/README.md index 6e018e7..1de4145 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/cli/cmd/wallet.go b/cli/cmd/wallet.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/cli/cmd/wallet.go @@ -0,0 +1 @@ +package cmd diff --git a/core/bitset.go b/core/bitset.go index 2beb2e0..d2fd593 100644 --- a/core/bitset.go +++ b/core/bitset.go @@ -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 @@ -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 } diff --git a/core/nakamoto/blockdag.go b/core/nakamoto/blockdag.go index 2148dea..84e9d0c 100644 --- a/core/nakamoto/blockdag.go +++ b/core/nakamoto/blockdag.go @@ -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 } @@ -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 @@ -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) } @@ -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. @@ -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 @@ -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(), ) @@ -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(), ) diff --git a/core/nakamoto/blockdag_test.go b/core/nakamoto/blockdag_test.go index 059bd99..49390af 100644 --- a/core/nakamoto/blockdag_test.go +++ b/core/nakamoto/blockdag_test.go @@ -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.") } @@ -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) { @@ -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) @@ -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) diff --git a/core/nakamoto/genesis.go b/core/nakamoto/genesis.go index 9e80197..b6a74ec 100644 --- a/core/nakamoto/genesis.go +++ b/core/nakamoto/genesis.go @@ -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. @@ -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 } diff --git a/core/nakamoto/genesis_test.go b/core/nakamoto/genesis_test.go index f534ff1..eefb3e9 100644 --- a/core/nakamoto/genesis_test.go +++ b/core/nakamoto/genesis_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/liamzebedee/tinychain-go/core" "github.com/stretchr/testify/assert" ) @@ -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") } diff --git a/core/nakamoto/miner.go b/core/nakamoto/miner.go index 94103fa..e1069db 100644 --- a/core/nakamoto/miner.go +++ b/core/nakamoto/miner.go @@ -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, } @@ -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 { @@ -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) diff --git a/core/nakamoto/pow_test.go b/core/nakamoto/pow_test.go index 42337f7..ee15415 100644 --- a/core/nakamoto/pow_test.go +++ b/core/nakamoto/pow_test.go @@ -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() diff --git a/core/nakamoto/state_machine.go b/core/nakamoto/state_machine.go index 8ce0c8f..727d6de 100644 --- a/core/nakamoto/state_machine.go +++ b/core/nakamoto/state_machine.go @@ -30,6 +30,9 @@ type StateMachineInput struct { // Miner address for fees. MinerPubkey [65]byte + + // Block reward. + BlockReward uint64 } // The state machine is the core of the business logic for the Nakamoto blockchain. @@ -64,6 +67,12 @@ func (c *StateMachine) Transition(input StateMachineInput) ([]*StateLeaf, error) return nil, errors.New("unsupported transaction version") } + // Check coinbase constraints. + if input.IsCoinbase && input.RawTransaction.Amount != input.BlockReward { + // TODO: this is a sanity check. + return nil, errors.New("invalid coinbase tx, amount must equal block reward") + } + if input.IsCoinbase { return c.transitionCoinbase(input) } else { @@ -136,15 +145,18 @@ func (c *StateMachine) transitionTransfer(input StateMachineInput) ([]*StateLeaf func (c *StateMachine) transitionCoinbase(input StateMachineInput) ([]*StateLeaf, error) { toBalance := c.GetBalance(input.RawTransaction.ToPubkey) - amount := input.RawTransaction.Amount + blockReward := input.BlockReward + + // TODO: what happens when blockreward != input.tx.amount?? + // TODO: what happens when blockreward == 0? // Check if the `to` balance will overflow. - if _, carry := bits.Add64(toBalance, amount, 0); carry != 0 { + if _, carry := bits.Add64(toBalance, blockReward, 0); carry != 0 { return nil, ErrToBalanceOverflow } // Add the coins to the `to` account balance. - toBalance += amount + toBalance += blockReward // Create the new state leaves. toLeaf := &StateLeaf{ @@ -172,7 +184,7 @@ func (c *StateMachine) GetState() map[[65]byte]uint64 { // Given a block DAG and a list of block hashes, extracts the transaction sequence, applies each transaction in order, and returns the final state. func RebuildState(dag *BlockDAG, stateMachine StateMachine, longestChainHashList [][32]byte) (*StateMachine, error) { - for _, blockHash := range longestChainHashList { + for blockHeight, blockHash := range longestChainHashList { // 1. Get all transactions for block. // TODO ignore: nonce, sig txs, err := dag.GetBlockTransactions(blockHash) @@ -180,12 +192,17 @@ func RebuildState(dag *BlockDAG, stateMachine StateMachine, longestChainHashList return nil, err } + if len(*txs) == 0 { + return nil, fmt.Errorf("Block %x has no transactions", blockHash) + } + // stateMachineLogger.Printf("Processing block %x with %d transactions", blockHash, len(*txs)) // 2. Map transactions to state leaves through state machine transition function. var stateMachineInput StateMachineInput var minerPubkey [65]byte isCoinbase := false + blockReward := GetBlockReward(blockHeight) for i, tx := range *txs { // Special case: coinbase tx is always the first tx in the block. @@ -199,6 +216,7 @@ func RebuildState(dag *BlockDAG, stateMachine StateMachine, longestChainHashList RawTransaction: tx.ToRawTransaction(), IsCoinbase: isCoinbase, MinerPubkey: minerPubkey, + BlockReward: blockReward, } // Transition the state machine. diff --git a/core/nakamoto/state_machine_test.go b/core/nakamoto/state_machine_test.go index f550e7f..4635f83 100644 --- a/core/nakamoto/state_machine_test.go +++ b/core/nakamoto/state_machine_test.go @@ -64,9 +64,10 @@ func TestStateMachineIdea(t *testing.T) { // Assert balances. // Ingest some transactions and calculate the state. tx0 := StateMachineInput{ - RawTransaction: MakeTransferTx(wallets[0].PubkeyBytes(), wallets[0].PubkeyBytes(), 100, &wallets[0], 0), + RawTransaction: MakeTransferTx(wallets[0].PubkeyBytes(), wallets[0].PubkeyBytes(), 100, 0, &wallets[0]), IsCoinbase: true, MinerPubkey: [65]byte{}, + BlockReward: 100, } effects, err := stateMachine.Transition(tx0) if err != nil { @@ -80,9 +81,10 @@ func TestStateMachineIdea(t *testing.T) { // Now transfer coins to another account. tx1 := StateMachineInput{ - RawTransaction: MakeTransferTx(wallets[0].PubkeyBytes(), wallets[1].PubkeyBytes(), 50, &wallets[0], 0), + RawTransaction: MakeTransferTx(wallets[0].PubkeyBytes(), wallets[1].PubkeyBytes(), 50, 0, &wallets[0]), IsCoinbase: false, MinerPubkey: [65]byte{}, + BlockReward: 0, } effects, err = stateMachine.Transition(tx1) if err != nil { @@ -309,6 +311,7 @@ func TestBenchmarkTxOpsPerDay(t *testing.T) { RawTransaction: newUnsignedTransferTx(wallets[0].PubkeyBytes(), wallets[0].PubkeyBytes(), 100, &wallets[0], 0), IsCoinbase: true, MinerPubkey: [65]byte{}, + BlockReward: 100, } effects, err := stateMachine.Transition(coinbaseTx) if err != nil { @@ -322,6 +325,7 @@ func TestBenchmarkTxOpsPerDay(t *testing.T) { RawTransaction: newUnsignedTransferTx(wallets[0].PubkeyBytes(), wallets[1].PubkeyBytes(), 50, &wallets[0], 0), IsCoinbase: false, MinerPubkey: [65]byte{}, + BlockReward: 0, } effects, err = stateMachine.Transition(tx1) if err != nil { @@ -452,6 +456,7 @@ func TestStateMachineReconstructState(t *testing.T) { RawTransaction: tx.ToRawTransaction(), IsCoinbase: isCoinbase, MinerPubkey: minerPubkey, + BlockReward: 0, } // Transition the state machine. @@ -478,8 +483,9 @@ func TestStateMachineReconstructState(t *testing.T) { } func assertIntEqual[num int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](t *testing.T, a num, b num) { + t.Helper() if a != b { - t.Fatalf("Expected %d to equal %d", a, b) + t.Errorf("Expected %d to equal %d", a, b) } } @@ -512,12 +518,12 @@ func TestStateMachineTxAlreadySequenced(t *testing.T) { t.Fatalf("Failed to rebuild state: %s\n", err) } - wallet0_balance := state.GetBalance(wallets[0].PubkeyBytes()) - assertIntEqual(t, uint64(50*10), wallet0_balance) // coinbase rewards. + wallet0_balance1 := state.GetBalance(wallets[0].PubkeyBytes()) + assertIntEqual(t, uint64(50*10)*ONE_COIN, wallet0_balance1) // coinbase rewards. // Now we send a transfer tx. // First create the tx, then mine a block with it. - rawTx := MakeTransferTx(wallets[0].PubkeyBytes(), wallets[1].PubkeyBytes(), 100, &wallets[0], 0) + rawTx := MakeTransferTx(wallets[0].PubkeyBytes(), wallets[1].PubkeyBytes(), 100, 0, &wallets[0]) miner.GetBlockBody = func() BlockBody { return []RawTransaction{rawTx} } @@ -541,7 +547,8 @@ func TestStateMachineTxAlreadySequenced(t *testing.T) { // Check the transfer tx was processed. wallet0_balance2 := state2.GetBalance(wallets[0].PubkeyBytes()) wallet1_balance1 := state2.GetBalance(wallets[1].PubkeyBytes()) - assertIntEqual(t, uint64(450), wallet0_balance2) + blockReward := GetBlockReward(int(dag.FullTip.Height)) + assertIntEqual(t, wallet0_balance1+blockReward-100, wallet0_balance2) assertIntEqual(t, uint64(100), wallet1_balance1) // Now we test transaction replay. @@ -550,6 +557,7 @@ func TestStateMachineTxAlreadySequenced(t *testing.T) { RawTransaction: rawTx, IsCoinbase: false, MinerPubkey: miner.CoinbaseWallet.PubkeyBytes(), + BlockReward: 0, } t.Skip() diff --git a/core/nakamoto/tokenomics.go b/core/nakamoto/tokenomics.go index 0f4fff2..14f82e2 100644 --- a/core/nakamoto/tokenomics.go +++ b/core/nakamoto/tokenomics.go @@ -4,9 +4,10 @@ import ( "math" ) -// GetBlockReward returns the block reward for a given block height. +// GetBlockReward returns the block reward in coins for a given block height. // It uses the standard Bitcoin inflation curve. -func GetBlockReward(blockHeight int) float64 { +// TODO URGENT: implement this in integer arithmetic to avoid precision differences causing consensus faults. +func GetBlockReward(blockHeight int) uint64 { initialReward := 50.0 halvingInterval := 210000 @@ -14,6 +15,12 @@ func GetBlockReward(blockHeight int) float64 { numHalvings := blockHeight / halvingInterval // Calculate the reward after the halvings - reward := initialReward / math.Pow(2, float64(numHalvings)) + reward_ := initialReward / math.Pow(2, float64(numHalvings)) + reward := uint64(reward_ * ONE_COIN) return reward } + +// ONE_COIN is the number of satoshis in one coin. +// Coin amounts are fixed-precision - they have 8 decimal places. +// 1 BTC = 1 * 10^8 = 100 000 000 sats +const ONE_COIN = 100_000_000 diff --git a/core/nakamoto/tokenomics_test.go b/core/nakamoto/tokenomics_test.go index 6ebf546..bc5a874 100644 --- a/core/nakamoto/tokenomics_test.go +++ b/core/nakamoto/tokenomics_test.go @@ -13,7 +13,7 @@ func TestGetBlockReward(t *testing.T) { blocksIn8Years := 1 * 6 * 24 * 365 * 120 xy := make([][2]float64, blocksIn8Years) for i := 0; i < blocksIn8Years; i++ { - xy[i] = [2]float64{float64(i), GetBlockReward(i)} + xy[i] = [2]float64{float64(i), float64(GetBlockReward(i))} } // Dump this to a csv for visualisation in the IPython notebook. diff --git a/core/nakamoto/tx.go b/core/nakamoto/tx.go index ae693fb..60cba5c 100644 --- a/core/nakamoto/tx.go +++ b/core/nakamoto/tx.go @@ -98,7 +98,7 @@ func (tx *RawTransaction) Hash() [32]byte { return sha256.Sum256(h.Sum(nil)) } -func MakeTransferTx(from [65]byte, to [65]byte, amount uint64, wallet *core.Wallet, fee uint64) RawTransaction { +func MakeTransferTx(from [65]byte, to [65]byte, amount uint64, fee uint64, wallet *core.Wallet) RawTransaction { tx := RawTransaction{ Version: 1, Sig: [64]byte{}, diff --git a/core/nakamoto/utils.go b/core/nakamoto/utils.go index e1d4c2a..0a955e8 100644 --- a/core/nakamoto/utils.go +++ b/core/nakamoto/utils.go @@ -119,11 +119,11 @@ func DiscoverIP() (string, int, error) { // Constructs a new logger with the given `prefix` and an optional `prefix2`. // // Format 1: -// prefix="prefix" prefix2="" +// NewLogger("prefix", "") // 2024/06/30 00:56:06 [prefix] message // // Format 2: -// prefix="prefix" prefix2="prefix2" +// NewLogger("prefix", "prefix2") // 2024/06/30 00:56:06 [prefix] (prefix2) message func NewLogger(prefix string, prefix2 string) *log.Logger { prefixFull := color.HiGreenString(fmt.Sprintf("[%s] ", prefix)) diff --git a/core/tendermint/consensus_test.go b/core/tendermint/consensus_test.go deleted file mode 100644 index db18232..0000000 --- a/core/tendermint/consensus_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// Tendermint/CometBFT consensus WIP. -// https://github.com/tendermint/tendermint/blob/main/spec/consensus/consensus.md#common-exit-conditions -// https://docs.tendermint.com/v0.34/introduction/what-is-tendermint.html# - -/* -# The consensus engine runs a consensus algorithm called Tendermint. -# Tendermint is a byzantine fault-tolerant consensus algorithm. -# It consists of a validator set, where each validator is a node with a public key and some voting power. -# Transmuted into a blockchain, Tendermint is a proof-of-stake consensus protocol. -# Voting power corresponds to staked token balance. -# -# [1]: https://github.com/tendermint/tendermint/blob/v0.34.x/spec/consensus/consensus.md -class TendermintConsensusEngine: - def __init__(self, node): - self.node = node - -# vset - the validator set -# n - the number of validators -# VP(i) - voting power of validator i -# A(i) - accumulated priority for validator i -# P - total voting power of set -# avg - average of all validator priorities -# prop - proposer -def voting_power(i): - return 0 - -# Select the proposer for the next epoch, from a dynamic validator set and -# the history of past proposers (priority). -# [1]: https://github.com/tendermint/tendermint/blob/v0.34.x/spec/consensus/proposer-selection.md -def ProposerSelection(vset, priority): - A = priority - A2 = priority.copy() - - # P - total voting power of set - P = sum(voting_power(i) for i in vset) - - # scale the priority values - diff = max(A) - min(A) - threshold = 2 * P - if diff > threshold: - scale = diff/threshold - for validator in vset: - i = validator - A2[i] = A[i]/scale - - # center priorities around zero - avg = sum(A(i) for i in vset)/len(vset) - for validator in vset: - i = validator - A2[i] -= avg - - # compute priorities and elect proposer - for validator in vset: - i = validator - A2[i] += voting_power(i) - - prop = max(A) - A2[prop] -= P -*/ - -package tendermint - -import ( - "testing" -) - -func TestTendermint(t *testing.T) { - -} diff --git a/core/wallet.go b/core/wallet.go index 3985f0c..54d3641 100644 --- a/core/wallet.go +++ b/core/wallet.go @@ -98,23 +98,13 @@ func (w *Wallet) Sign(msg []byte) ([]byte, error) { } // Verifies an ECDSA signature for a message using the public key. -func VerifySignature(pubkeyStr string, sig, msg []byte) bool { +func VerifySignature(pubkeyBytes [65]byte, sig, msg []byte) bool { if len(sig) != 64 { fmt.Printf("Invalid signature length: %d\n", len(sig)) // TODO return false } - if len(pubkeyStr) != 130 { - panic("Invalid public key") // TODO - // return false - } - - pubkeyBytes, err := hex.DecodeString(pubkeyStr) - if err != nil { - panic(err) - // return false - } - x, y := elliptic.Unmarshal(elliptic.P256(), pubkeyBytes) + x, y := elliptic.Unmarshal(elliptic.P256(), pubkeyBytes[:]) if x == nil { panic("Invalid public key") // TODO // return false diff --git a/core/wallet_test.go b/core/wallet_test.go index 996c0f7..1b05099 100644 --- a/core/wallet_test.go +++ b/core/wallet_test.go @@ -67,7 +67,11 @@ func TestVerifyWithRealSig(t *testing.T) { t.Fatalf("Failed to decode signature: %s", err) } - ok := VerifySignature(pubkeyStr, sig, msg) + var pubkeyBytes [65]byte + pubkeyBytes1, _ := hex.DecodeString(pubkeyStr) + copy(pubkeyBytes[:], pubkeyBytes1) + + ok := VerifySignature(pubkeyBytes, sig, msg) assert.True(ok) } @@ -92,8 +96,7 @@ func TestVerify(t *testing.T) { t.Logf("Signature: %s", hex.EncodeToString(sig)) // Verify the signature. - pubkeyStr := wallet.PubkeyStr() - ok := VerifySignature(pubkeyStr, sig, msg) + ok := VerifySignature(wallet.PubkeyBytes(), sig, msg) assert.True(ok) } diff --git a/docs/differences-from-bitcoin.md b/docs/differences-from-bitcoin.md index fc0325d..b03e509 100644 --- a/docs/differences-from-bitcoin.md +++ b/docs/differences-from-bitcoin.md @@ -10,6 +10,7 @@ Differences: * Transactions do not have a VM environment. * The state model is not based on UXTO's or accounts. Tinychain computes state like an account-based chain, in that it stores an `account -> balance` mapping. But internally, it stores its state as state leafs - which are more similar to unique UXTO's than in Ethereum's model of accounts. + * Bitcoin features protection against quantum computing attacks, since coins are locked to a preimage of a public key (RIPEMD(SHA256(pubkey))) using P2PKH rather than locked to a public key itself. Missing efficiencies: