/
block.go
278 lines (223 loc) · 9.77 KB
/
block.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package database
import (
"context"
"crypto/rand"
"errors"
"fmt"
"math"
"math/big"
"time"
"github.com/ardanlabs/blockchain/foundation/blockchain/merkle"
"github.com/ardanlabs/blockchain/foundation/blockchain/signature"
)
// ErrChainForked is returned from validateNextBlock if another node's chain
// is two or more blocks ahead of ours.
var ErrChainForked = errors.New("blockchain forked, start resync")
// =============================================================================
// BlockData represents what can be serialized to disk and over the network.
type BlockData struct {
Hash string `json:"hash"`
Header BlockHeader `json:"block"`
Trans []BlockTx `json:"trans"`
}
// NewBlockData constructs block data from a block.
func NewBlockData(block Block) BlockData {
blockData := BlockData{
Hash: block.Hash(),
Header: block.Header,
Trans: block.MerkleTree.Values(),
}
return blockData
}
// ToBlock converts a storage block into a database block.
func ToBlock(blockData BlockData) (Block, error) {
tree, err := merkle.NewTree(blockData.Trans)
if err != nil {
return Block{}, err
}
block := Block{
Header: blockData.Header,
MerkleTree: tree,
}
return block, nil
}
// =============================================================================
// BlockHeader represents common information required for each block.
type BlockHeader struct {
Number uint64 `json:"number"` // Ethereum: Block number in the chain.
PrevBlockHash string `json:"prev_block_hash"` // Bitcoin: Hash of the previous block in the chain.
TimeStamp uint64 `json:"timestamp"` // Bitcoin: Time the block was mined.
BeneficiaryID AccountID `json:"beneficiary"` // Ethereum: The account who is receiving fees and tips.
Difficulty uint16 `json:"difficulty"` // Ethereum: Number of 0's needed to solve the hash solution.
MiningReward uint64 `json:"mining_reward"` // Ethereum: The reward for mining this block.
StateRoot string `json:"state_root"` // Ethereum: Represents a hash of the accounts and their balances.
TransRoot string `json:"trans_root"` // Both: Represents the merkle tree root hash for the transactions in this block.
Nonce uint64 `json:"nonce"` // Both: Value identified to solve the hash solution.
}
// Block represents a group of transactions batched together.
type Block struct {
Header BlockHeader
MerkleTree *merkle.Tree[BlockTx]
}
// POWArgs represents the set of arguments required to run POW.
type POWArgs struct {
BeneficiaryID AccountID
Difficulty uint16
MiningReward uint64
PrevBlock Block
StateRoot string
Trans []BlockTx
EvHandler func(v string, args ...any)
}
// POW constructs a new Block and performs the work to find a nonce that
// solves the cryptographic POW puzzel.
func POW(ctx context.Context, args POWArgs) (Block, error) {
// When mining the first block, the previous block's hash will be zero.
prevBlockHash := signature.ZeroHash
if args.PrevBlock.Header.Number > 0 {
prevBlockHash = args.PrevBlock.Hash()
}
// Construct a merkle tree from the transaction for this block. The root
// of this tree will be part of the block to be mined.
tree, err := merkle.NewTree(args.Trans)
if err != nil {
return Block{}, err
}
// Construct the block to be mined.
block := Block{
Header: BlockHeader{
Number: args.PrevBlock.Header.Number + 1,
PrevBlockHash: prevBlockHash,
TimeStamp: uint64(time.Now().UTC().UnixMilli()),
BeneficiaryID: args.BeneficiaryID,
Difficulty: args.Difficulty,
MiningReward: args.MiningReward,
StateRoot: args.StateRoot,
TransRoot: tree.RootHex(), //
Nonce: 0, // Will be identified by the POW algorithm.
},
MerkleTree: tree,
}
// Peform the proof of work mining operation.
if err := block.performPOW(ctx, args.EvHandler); err != nil {
return Block{}, err
}
return block, nil
}
// performPOW does the work of mining to find a valid hash for a specified
// block. Pointer semantics are being used since a nonce is being discovered.
func (b *Block) performPOW(ctx context.Context, ev func(v string, args ...any)) error {
ev("database: PerformPOW: MINING: started")
defer ev("database: PerformPOW: MINING: completed")
// Log the transactions that are a part of this potential block.
for _, tx := range b.MerkleTree.Values() {
ev("database: PerformPOW: MINING: tx[%s]", tx)
}
// Choose a random starting point for the nonce. After this, the nonce
// will be incremented by 1 until a solution is found by us or another node.
nBig, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return ctx.Err()
}
b.Header.Nonce = nBig.Uint64()
ev("viewer: PerformPOW: MINING: running")
// Loop until we or another node finds a solution for the next block.
var attempts uint64
for {
attempts++
if attempts%1_000_000 == 0 {
ev("viewer: PerformPOW: MINING: running: attempts[%d]", attempts)
}
// Did we timeout trying to solve the problem.
if ctx.Err() != nil {
ev("database: PerformPOW: MINING: CANCELLED")
return ctx.Err()
}
// Hash the block and check if we have solved the puzzle.
hash := b.Hash()
if !isHashSolved(b.Header.Difficulty, hash) {
b.Header.Nonce++
continue
}
ev("database: PerformPOW: MINING: SOLVED: prevBlk[%s]: newBlk[%s]", b.Header.PrevBlockHash, hash)
ev("database: PerformPOW: MINING: attempts[%d]", attempts)
return nil
}
}
// Hash returns the unique hash for the Block.
func (b Block) Hash() string {
if b.Header.Number == 0 {
return signature.ZeroHash
}
// CORE NOTE: Hashing the block header and not the whole block so the blockchain
// can be cryptographically checked by only needing block headers and not full
// blocks with the transaction data. This will support the ability to have pruned
// nodes and light clients in the future.
// - A pruned node stores all the block headers, but only a small number of full
// blocks (maybe the last 1000 blocks). This allows for full cryptographic
// validation of blocks and transactions without all the extra storage.
// - A light client keeps block headers and just enough sufficient information
// to follow the latest set of blocks being produced. The do not validate
// blocks, but can prove a transaction is in a block.
return signature.Hash(b.Header)
}
// ValidateBlock takes a block and validates it to be included into the blockchain.
func (b Block) ValidateBlock(previousBlock Block, stateRoot string, evHandler func(v string, args ...any)) error {
evHandler("database: ValidateBlock: validate: blk[%d]: check: chain is not forked", b.Header.Number)
// The node who sent this block has a chain that is two or more blocks ahead
// of ours. This means there has been a fork and we are on the wrong side.
nextNumber := previousBlock.Header.Number + 1
if b.Header.Number >= (nextNumber + 2) {
return ErrChainForked
}
evHandler("database: ValidateBlock: validate: blk[%d]: check: block difficulty is the same or greater than parent block difficulty", b.Header.Number)
if b.Header.Difficulty < previousBlock.Header.Difficulty {
return fmt.Errorf("block difficulty is less than previous block difficulty, parent %d, block %d", previousBlock.Header.Difficulty, b.Header.Difficulty)
}
evHandler("database: ValidateBlock: validate: blk[%d]: check: block hash has been solved", b.Header.Number)
hash := b.Hash()
if !isHashSolved(b.Header.Difficulty, hash) {
return fmt.Errorf("%s invalid block hash", hash)
}
evHandler("database: ValidateBlock: validate: blk[%d]: check: block number is the next number", b.Header.Number)
if b.Header.Number != nextNumber {
return fmt.Errorf("this block is not the next number, got %d, exp %d", b.Header.Number, nextNumber)
}
evHandler("database: ValidateBlock: validate: blk[%d]: check: parent hash does match parent block", b.Header.Number)
if b.Header.PrevBlockHash != previousBlock.Hash() {
return fmt.Errorf("parent block hash doesn't match our known parent, got %s, exp %s", b.Header.PrevBlockHash, previousBlock.Hash())
}
if previousBlock.Header.TimeStamp > 0 {
evHandler("database: ValidateBlock: validate: blk[%d]: check: block's timestamp is greater than parent block's timestamp", b.Header.Number)
parentTime := time.Unix(int64(previousBlock.Header.TimeStamp), 0)
blockTime := time.Unix(int64(b.Header.TimeStamp), 0)
if blockTime.Before(parentTime) {
return fmt.Errorf("block timestamp is before parent block, parent %s, block %s", parentTime, blockTime)
}
// This is a check that Ethereum does but we can't because we don't run all the time.
// evHandler("database: ValidateBlock: validate: blk[%d]: check: block is less than 15 minutes apart from parent block", b.Header.Number)
// dur := blockTime.Sub(parentTime)
// if dur.Seconds() > time.Duration(15*time.Second).Seconds() {
// return fmt.Errorf("block is older than 15 minutes, duration %v", dur)
// }
}
evHandler("database: ValidateBlock: validate: blk[%d]: check: state root hash does match current database", b.Header.Number)
if b.Header.StateRoot != stateRoot {
return fmt.Errorf("state of the accounts are wrong, current %s, expected %s", stateRoot, b.Header.StateRoot)
}
evHandler("database: ValidateBlock: validate: blk[%d]: check: merkle root does match transactions", b.Header.Number)
if b.Header.TransRoot != b.MerkleTree.RootHex() {
return fmt.Errorf("merkle root does not match transactions, got %s, exp %s", b.MerkleTree.RootHex(), b.Header.TransRoot)
}
return nil
}
// isHashSolved checks the hash to make sure it complies with
// the POW rules. We need to match a difficulty number of 0's.
func isHashSolved(difficulty uint16, hash string) bool {
const match = "0x00000000000000000"
if len(hash) != 66 {
return false
}
difficulty += 2
return hash[:difficulty] == match[:difficulty]
}