From 73dda1eb9d18e7c05f0c7445d47a430039457cbe Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 15:29:37 +1000 Subject: [PATCH 01/16] impl block reward --- core/nakamoto/blockdag.go | 12 +++++++++++- core/nakamoto/state_machine.go | 17 +++++++++++++---- core/nakamoto/tokenomics.go | 5 +++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/core/nakamoto/blockdag.go b/core/nakamoto/blockdag.go index 2148dea..7b9906e 100644 --- a/core/nakamoto/blockdag.go +++ b/core/nakamoto/blockdag.go @@ -346,7 +346,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,6 +385,11 @@ 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 { @@ -506,6 +511,11 @@ 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 { diff --git a/core/nakamoto/state_machine.go b/core/nakamoto/state_machine.go index 8ce0c8f..fd54c19 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. @@ -136,15 +139,15 @@ 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 // 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 +175,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 +183,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 := uint64(GetBlockReward(blockHeight) * ONE_COIN) for i, tx := range *txs { // Special case: coinbase tx is always the first tx in the block. @@ -199,6 +207,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/tokenomics.go b/core/nakamoto/tokenomics.go index 0f4fff2..87b4f66 100644 --- a/core/nakamoto/tokenomics.go +++ b/core/nakamoto/tokenomics.go @@ -17,3 +17,8 @@ func GetBlockReward(blockHeight int) float64 { reward := initialReward / math.Pow(2, float64(numHalvings)) return reward } + +// 1 BTC = 1 * 10^8 +// BTC amounts are fixed-precision - they have 8 decimal places. +// ie. 1 BTC = 100 000 000 sats +const ONE_COIN = 100_000_000 From 14f4c8299e814270c1872112bd5716bb6c783893 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 15:31:00 +1000 Subject: [PATCH 02/16] update test w blockreward --- core/nakamoto/state_machine_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/nakamoto/state_machine_test.go b/core/nakamoto/state_machine_test.go index f550e7f..82788c6 100644 --- a/core/nakamoto/state_machine_test.go +++ b/core/nakamoto/state_machine_test.go @@ -67,6 +67,7 @@ func TestStateMachineIdea(t *testing.T) { RawTransaction: MakeTransferTx(wallets[0].PubkeyBytes(), wallets[0].PubkeyBytes(), 100, &wallets[0], 0), IsCoinbase: true, MinerPubkey: [65]byte{}, + BlockReward: 0, } effects, err := stateMachine.Transition(tx0) if err != nil { @@ -83,6 +84,7 @@ func TestStateMachineIdea(t *testing.T) { RawTransaction: MakeTransferTx(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 { @@ -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: 0, } 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. @@ -550,6 +555,7 @@ func TestStateMachineTxAlreadySequenced(t *testing.T) { RawTransaction: rawTx, IsCoinbase: false, MinerPubkey: miner.CoinbaseWallet.PubkeyBytes(), + BlockReward: 0, } t.Skip() From 8e11f19c19115cc7d459dbdef2caf4dfd4454cc3 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 20:50:34 +1100 Subject: [PATCH 03/16] doc --- README.md | 2 ++ core/nakamoto/blockdag.go | 16 ++++++++++++++++ docs/differences-from-bitcoin.md | 1 + 3 files changed, 19 insertions(+) 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/core/nakamoto/blockdag.go b/core/nakamoto/blockdag.go index 2148dea..bdaca27 100644 --- a/core/nakamoto/blockdag.go +++ b/core/nakamoto/blockdag.go @@ -211,6 +211,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. 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: From 8ff7dd4ce67ccda768aa385be7e3b8c952c676e4 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 22:38:23 +1100 Subject: [PATCH 04/16] doc: fix bitset docs --- core/bitset.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 } From 9c4c332223db848c0a765cb480fee9ec48f807ae Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 22:38:32 +1100 Subject: [PATCH 05/16] rm tendermint stuff --- core/tendermint/consensus_test.go | 69 ------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 core/tendermint/consensus_test.go 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) { - -} From cea349ff44bf3929fb2fccc09e5992e305e869e7 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 22:38:43 +1100 Subject: [PATCH 06/16] add coinbase tx to genesis block --- core/nakamoto/genesis.go | 32 +++++++++-------------------- core/nakamoto/genesis_test.go | 38 +++++++++++++++++++++++++++++++++++ core/nakamoto/miner.go | 11 +++++----- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/core/nakamoto/genesis.go b/core/nakamoto/genesis.go index 9e80197..52e6e47 100644 --- a/core/nakamoto/genesis.go +++ b/core/nakamoto/genesis.go @@ -35,7 +35,16 @@ func GetRawGenesisBlockFromConfig(consensus ConsensusConfig) RawBlock { TransactionsMerkleRoot: [32]byte{}, 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: []RawTransaction{ + { + FromPubkey: [65]byte{0x04, 0xd0, 0xe7, 0x0c, 0xe1, 0xed, 0x48, 0x3e, 0x2f, 0x32, 0xad, 0x55, 0x11, 0x53, 0xa5, 0x36, 0x3f, 0xa6, 0xc8, 0x2f, 0xd5, 0xea, 0x0c, 0xd9, 0x5b, 0xf8, 0x0e, 0xae, 0xb3, 0x44, 0x15, 0xcb, 0x8d, 0x45, 0xc3, 0x93, 0x1f, 0xcc, 0x49, 0xed, 0x44, 0x4b, 0xe5, 0x44, 0x0f, 0x1d, 0x52, 0x23, 0x4a, 0x7e, 0xb2, 0xbd, 0x9d, 0xc3, 0x7a, 0x40, 0x90, 0x69, 0x79, 0xa5, 0x2f, 0xa0, 0x72, 0xe5, 0xb4}, + ToPubkey: [65]byte{0x04, 0xd0, 0xe7, 0x0c, 0xe1, 0xed, 0x48, 0x3e, 0x2f, 0x32, 0xad, 0x55, 0x11, 0x53, 0xa5, 0x36, 0x3f, 0xa6, 0xc8, 0x2f, 0xd5, 0xea, 0x0c, 0xd9, 0x5b, 0xf8, 0x0e, 0xae, 0xb3, 0x44, 0x15, 0xcb, 0x8d, 0x45, 0xc3, 0x93, 0x1f, 0xcc, 0x49, 0xed, 0x44, 0x4b, 0xe5, 0x44, 0x0f, 0x1d, 0x52, 0x23, 0x4a, 0x7e, 0xb2, 0xbd, 0x9d, 0xc3, 0x7a, 0x40, 0x90, 0x69, 0x79, 0xa5, 0x2f, 0xa0, 0x72, 0xe5, 0xb4}, + Sig: [64]byte{0xb3, 0x4c, 0x36, 0x1d, 0xb2, 0xbe, 0x89, 0x48, 0xfb, 0xd6, 0x61, 0x9e, 0xa5, 0xeb, 0xe2, 0x7b, 0x90, 0xae, 0x8d, 0x5a, 0xca, 0xa6, 0x94, 0xc2, 0x1f, 0x11, 0x81, 0x7e, 0x16, 0x98, 0x17, 0x41, 0xeb, 0x6a, 0xe8, 0xc4, 0xbf, 0x48, 0xe3, 0x13, 0x99, 0x81, 0x0e, 0xec, 0xb9, 0x62, 0x69, 0x8d, 0x8d, 0xb1, 0x15, 0x3b, 0xfb, 0x0d, 0x67, 0x6c, 0xa8, 0x6e, 0x52, 0x55, 0x1e, 0xf4, 0x27, 0x8d}, + Amount: 50, + Fee: 0, + Nonce: 0, + }, + }, } // Mine the block. @@ -55,26 +64,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..c858953 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" ) @@ -39,3 +40,40 @@ func TestGetRawGenesisBlockFromConfig(t *testing.T) { assert.Equal([32]byte{}, genesisBlock.TransactionsMerkleRoot) assert.Equal(big.NewInt(21).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, uint64(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("From: %v,\n", formatByteArrayDynamic(tx.FromPubkey[:])) + fmt.Printf("To: %v,\n", formatByteArrayDynamic(tx.ToPubkey[:])) + fmt.Printf("Sig: %v,\n", formatByteArrayDynamic(tx.Sig[:])) + 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..be04d1f 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 := uint64(GetBlockReward(int(current_tip.Height))) + coinbaseTx := MakeCoinbaseTx(miner.CoinbaseWallet, blockReward) + // Get the block body. blockBody := []RawTransaction{} blockBody = append(blockBody, coinbaseTx) From 6144aa6d67ecf713dd6a29b54c8b41e0c3ba0756 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 22:46:30 +1100 Subject: [PATCH 07/16] fix --- core/nakamoto/genesis_test.go | 2 +- core/nakamoto/miner.go | 2 +- core/nakamoto/state_machine.go | 2 +- core/nakamoto/tokenomics.go | 13 +++++++------ core/nakamoto/tokenomics_test.go | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/core/nakamoto/genesis_test.go b/core/nakamoto/genesis_test.go index c858953..41331a1 100644 --- a/core/nakamoto/genesis_test.go +++ b/core/nakamoto/genesis_test.go @@ -58,7 +58,7 @@ func TestWalletCreateSignTransferTx(t *testing.T) { if err != nil { panic(err) } - tx := MakeCoinbaseTx(wallet, uint64(GetBlockReward(0))) + tx := MakeCoinbaseTx(wallet, GetBlockReward(0)) // JSON dump. // str, err := json.Marshal(tx) diff --git a/core/nakamoto/miner.go b/core/nakamoto/miner.go index be04d1f..e1069db 100644 --- a/core/nakamoto/miner.go +++ b/core/nakamoto/miner.go @@ -159,7 +159,7 @@ func (miner *Miner) MakeNewPuzzle() POWPuzzle { } // Construct coinbase tx. - blockReward := uint64(GetBlockReward(int(current_tip.Height))) + blockReward := GetBlockReward(int(current_tip.Height)) coinbaseTx := MakeCoinbaseTx(miner.CoinbaseWallet, blockReward) // Get the block body. diff --git a/core/nakamoto/state_machine.go b/core/nakamoto/state_machine.go index fd54c19..0807506 100644 --- a/core/nakamoto/state_machine.go +++ b/core/nakamoto/state_machine.go @@ -193,7 +193,7 @@ func RebuildState(dag *BlockDAG, stateMachine StateMachine, longestChainHashList var stateMachineInput StateMachineInput var minerPubkey [65]byte isCoinbase := false - blockReward := uint64(GetBlockReward(blockHeight) * ONE_COIN) + blockReward := GetBlockReward(blockHeight) for i, tx := range *txs { // Special case: coinbase tx is always the first tx in the block. diff --git a/core/nakamoto/tokenomics.go b/core/nakamoto/tokenomics.go index 87b4f66..30b934b 100644 --- a/core/nakamoto/tokenomics.go +++ b/core/nakamoto/tokenomics.go @@ -4,9 +4,9 @@ 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 { +func GetBlockReward(blockHeight int) uint64 { initialReward := 50.0 halvingInterval := 210000 @@ -14,11 +14,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 } -// 1 BTC = 1 * 10^8 -// BTC amounts are fixed-precision - they have 8 decimal places. -// ie. 1 BTC = 100 000 000 sats +// 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. From 19d4649f0c2d46192c53ccbdb24be9caa2637686 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 1 Jan 2025 23:16:54 +1100 Subject: [PATCH 08/16] wallet: VerifySig doesn't do string conversion anymore --- cli/cmd/wallet.go | 1 + core/nakamoto/blockdag.go | 10 ++++++++-- core/nakamoto/blockdag_test.go | 14 +++++++------- core/nakamoto/genesis.go | 26 ++++++++++++++------------ core/nakamoto/genesis_test.go | 7 ++++--- core/nakamoto/pow_test.go | 2 +- core/nakamoto/utils.go | 4 ++-- core/wallet.go | 14 ++------------ core/wallet_test.go | 9 ++++++--- 9 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 cli/cmd/wallet.go 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/nakamoto/blockdag.go b/core/nakamoto/blockdag.go index fdd4823..94ed00b 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 } @@ -411,7 +417,7 @@ func (dag *BlockDAG) IngestBlockBody(body []RawTransaction) error { 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(), ) @@ -537,7 +543,7 @@ func (dag *BlockDAG) IngestBlock(raw RawBlock) error { 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..fb9c3de 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.") } @@ -407,16 +407,16 @@ func TestDagGetBlockByHashGenesis(t *testing.T) { 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) { diff --git a/core/nakamoto/genesis.go b/core/nakamoto/genesis.go index 52e6e47..9009f22 100644 --- a/core/nakamoto/genesis.go +++ b/core/nakamoto/genesis.go @@ -25,26 +25,28 @@ type ConsensusConfig struct { // Builds the raw genesis block from the consensus configuration. 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{ - { - FromPubkey: [65]byte{0x04, 0xd0, 0xe7, 0x0c, 0xe1, 0xed, 0x48, 0x3e, 0x2f, 0x32, 0xad, 0x55, 0x11, 0x53, 0xa5, 0x36, 0x3f, 0xa6, 0xc8, 0x2f, 0xd5, 0xea, 0x0c, 0xd9, 0x5b, 0xf8, 0x0e, 0xae, 0xb3, 0x44, 0x15, 0xcb, 0x8d, 0x45, 0xc3, 0x93, 0x1f, 0xcc, 0x49, 0xed, 0x44, 0x4b, 0xe5, 0x44, 0x0f, 0x1d, 0x52, 0x23, 0x4a, 0x7e, 0xb2, 0xbd, 0x9d, 0xc3, 0x7a, 0x40, 0x90, 0x69, 0x79, 0xa5, 0x2f, 0xa0, 0x72, 0xe5, 0xb4}, - ToPubkey: [65]byte{0x04, 0xd0, 0xe7, 0x0c, 0xe1, 0xed, 0x48, 0x3e, 0x2f, 0x32, 0xad, 0x55, 0x11, 0x53, 0xa5, 0x36, 0x3f, 0xa6, 0xc8, 0x2f, 0xd5, 0xea, 0x0c, 0xd9, 0x5b, 0xf8, 0x0e, 0xae, 0xb3, 0x44, 0x15, 0xcb, 0x8d, 0x45, 0xc3, 0x93, 0x1f, 0xcc, 0x49, 0xed, 0x44, 0x4b, 0xe5, 0x44, 0x0f, 0x1d, 0x52, 0x23, 0x4a, 0x7e, 0xb2, 0xbd, 0x9d, 0xc3, 0x7a, 0x40, 0x90, 0x69, 0x79, 0xa5, 0x2f, 0xa0, 0x72, 0xe5, 0xb4}, - Sig: [64]byte{0xb3, 0x4c, 0x36, 0x1d, 0xb2, 0xbe, 0x89, 0x48, 0xfb, 0xd6, 0x61, 0x9e, 0xa5, 0xeb, 0xe2, 0x7b, 0x90, 0xae, 0x8d, 0x5a, 0xca, 0xa6, 0x94, 0xc2, 0x1f, 0x11, 0x81, 0x7e, 0x16, 0x98, 0x17, 0x41, 0xeb, 0x6a, 0xe8, 0xc4, 0xbf, 0x48, 0xe3, 0x13, 0x99, 0x81, 0x0e, 0xec, 0xb9, 0x62, 0x69, 0x8d, 0x8d, 0xb1, 0x15, 0x3b, 0xfb, 0x0d, 0x67, 0x6c, 0xa8, 0x6e, 0x52, 0x55, 0x1e, 0xf4, 0x27, 0x8d}, - Amount: 50, - Fee: 0, - Nonce: 0, - }, - }, + Transactions: txs, } // Mine the block. diff --git a/core/nakamoto/genesis_test.go b/core/nakamoto/genesis_test.go index 41331a1..7df03a7 100644 --- a/core/nakamoto/genesis_test.go +++ b/core/nakamoto/genesis_test.go @@ -69,9 +69,10 @@ func TestWalletCreateSignTransferTx(t *testing.T) { // Print as a Go-formatted RawTransaction{} for usage in genesis.go. fmt.Printf("Coinbase tx:\n") fmt.Printf("RawTransaction {\n") - fmt.Printf("From: %v,\n", formatByteArrayDynamic(tx.FromPubkey[:])) - fmt.Printf("To: %v,\n", formatByteArrayDynamic(tx.ToPubkey[:])) - fmt.Printf("Sig: %v,\n", formatByteArrayDynamic(tx.Sig[:])) + 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) 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/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/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) } From ea0ee3713d97b74b2b207408f355e4f2cb10168a Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 21:50:43 +1100 Subject: [PATCH 09/16] fix: test --- core/nakamoto/blockdag_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/nakamoto/blockdag_test.go b/core/nakamoto/blockdag_test.go index fb9c3de..00f964a 100644 --- a/core/nakamoto/blockdag_test.go +++ b/core/nakamoto/blockdag_test.go @@ -494,8 +494,8 @@ func TestDagBlockDAGInitialised(t *testing.T) { 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(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(21).String(), genesisNonce.String()) assert.Equal(uint64(0), block.Height) assert.Equal(GetIdForEpoch(genesisBlock.Hash(), 0), block.Epoch) @@ -523,7 +523,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) From c8bb5141b23c7c9780eab2dbdf0382f12f8c464b Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 21:50:59 +1100 Subject: [PATCH 10/16] fix: OnNewFullTip called twice --- core/nakamoto/blockdag.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/core/nakamoto/blockdag.go b/core/nakamoto/blockdag.go index 94ed00b..84e9d0c 100644 --- a/core/nakamoto/blockdag.go +++ b/core/nakamoto/blockdag.go @@ -169,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 @@ -188,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) } From 85c9f8b435b5e917dcdc8a4489b35bca67aea5ba Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 22:17:03 +1100 Subject: [PATCH 11/16] statemachine: add temporary sanity check --- core/nakamoto/state_machine.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/nakamoto/state_machine.go b/core/nakamoto/state_machine.go index 0807506..727d6de 100644 --- a/core/nakamoto/state_machine.go +++ b/core/nakamoto/state_machine.go @@ -67,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 { @@ -141,6 +147,9 @@ func (c *StateMachine) transitionCoinbase(input StateMachineInput) ([]*StateLeaf toBalance := c.GetBalance(input.RawTransaction.ToPubkey) 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, blockReward, 0); carry != 0 { return nil, ErrToBalanceOverflow From 07524ba0a0513332f6418163f2e8499b72465483 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 22:17:19 +1100 Subject: [PATCH 12/16] fix tests --- core/nakamoto/genesis.go | 4 ++++ core/nakamoto/genesis_test.go | 21 +++++++++++---------- core/nakamoto/state_machine_test.go | 9 +++++---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/core/nakamoto/genesis.go b/core/nakamoto/genesis.go index 9009f22..b6a74ec 100644 --- a/core/nakamoto/genesis.go +++ b/core/nakamoto/genesis.go @@ -24,6 +24,10 @@ 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{ diff --git a/core/nakamoto/genesis_test.go b/core/nakamoto/genesis_test.go index 7df03a7..eefb3e9 100644 --- a/core/nakamoto/genesis_test.go +++ b/core/nakamoto/genesis_test.go @@ -25,20 +25,21 @@ 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 { diff --git a/core/nakamoto/state_machine_test.go b/core/nakamoto/state_machine_test.go index 82788c6..d0e18d9 100644 --- a/core/nakamoto/state_machine_test.go +++ b/core/nakamoto/state_machine_test.go @@ -67,7 +67,7 @@ func TestStateMachineIdea(t *testing.T) { RawTransaction: MakeTransferTx(wallets[0].PubkeyBytes(), wallets[0].PubkeyBytes(), 100, &wallets[0], 0), IsCoinbase: true, MinerPubkey: [65]byte{}, - BlockReward: 0, + BlockReward: 100, } effects, err := stateMachine.Transition(tx0) if err != nil { @@ -311,7 +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: 0, + BlockReward: 100, } effects, err := stateMachine.Transition(coinbaseTx) if err != nil { @@ -484,7 +484,8 @@ 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) { if a != b { - t.Fatalf("Expected %d to equal %d", a, b) + t.Logf("Expected %d to equal %d", a, b) + t.FailNow() } } @@ -518,7 +519,7 @@ func TestStateMachineTxAlreadySequenced(t *testing.T) { } wallet0_balance := state.GetBalance(wallets[0].PubkeyBytes()) - assertIntEqual(t, uint64(50*10), wallet0_balance) // coinbase rewards. + assertIntEqual(t, uint64(50*10)*ONE_COIN, wallet0_balance) // coinbase rewards. // Now we send a transfer tx. // First create the tx, then mine a block with it. From c6f0405ffa98c99c310362b4bd33f008964faf77 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 22:17:28 +1100 Subject: [PATCH 13/16] fix tests --- core/nakamoto/blockdag_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/nakamoto/blockdag_test.go b/core/nakamoto/blockdag_test.go index 00f964a..ebf760d 100644 --- a/core/nakamoto/blockdag_test.go +++ b/core/nakamoto/blockdag_test.go @@ -404,6 +404,7 @@ 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) @@ -489,6 +490,7 @@ 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) From 65c8684a15b2431b9cd178277be83ae754903fc9 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 22:24:29 +1100 Subject: [PATCH 14/16] fix test --- core/nakamoto/blockdag_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nakamoto/blockdag_test.go b/core/nakamoto/blockdag_test.go index ebf760d..49390af 100644 --- a/core/nakamoto/blockdag_test.go +++ b/core/nakamoto/blockdag_test.go @@ -498,7 +498,7 @@ func TestDagBlockDAGInitialised(t *testing.T) { 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(21).String(), genesisNonce.String()) + assert.Equal(big.NewInt(19).String(), genesisNonce.String()) assert.Equal(uint64(0), block.Height) assert.Equal(GetIdForEpoch(genesisBlock.Hash(), 0), block.Epoch) From e0c14a889821f4b3b24c128745e3283bd8ce9c3e Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 22:51:37 +1100 Subject: [PATCH 15/16] tx: change order of args for MakeTransferTx --- core/nakamoto/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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{}, From b6e9c74d6a81093b2accd6b047c41908463a5a90 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Sat, 4 Jan 2025 22:51:48 +1100 Subject: [PATCH 16/16] fix test --- core/nakamoto/state_machine_test.go | 17 +++++++++-------- core/nakamoto/tokenomics.go | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/core/nakamoto/state_machine_test.go b/core/nakamoto/state_machine_test.go index d0e18d9..4635f83 100644 --- a/core/nakamoto/state_machine_test.go +++ b/core/nakamoto/state_machine_test.go @@ -64,7 +64,7 @@ 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, @@ -81,7 +81,7 @@ 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, @@ -483,9 +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.Logf("Expected %d to equal %d", a, b) - t.FailNow() + t.Errorf("Expected %d to equal %d", a, b) } } @@ -518,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)*ONE_COIN, 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} } @@ -547,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. diff --git a/core/nakamoto/tokenomics.go b/core/nakamoto/tokenomics.go index 30b934b..14f82e2 100644 --- a/core/nakamoto/tokenomics.go +++ b/core/nakamoto/tokenomics.go @@ -6,6 +6,7 @@ import ( // GetBlockReward returns the block reward in coins for a given block height. // It uses the standard Bitcoin inflation curve. +// TODO URGENT: implement this in integer arithmetic to avoid precision differences causing consensus faults. func GetBlockReward(blockHeight int) uint64 { initialReward := 50.0 halvingInterval := 210000