-
Notifications
You must be signed in to change notification settings - Fork 328
/
blockchain.go
550 lines (495 loc) · 16.1 KB
/
blockchain.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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
// Copyright (c) 2024 IoTeX Foundation
// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
// This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
package blockchain
import (
"context"
"math/big"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/facebookgo/clock"
"github.com/iotexproject/go-pkgs/hash"
"github.com/iotexproject/iotex-address/address"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"github.com/iotexproject/iotex-core/v2/action"
"github.com/iotexproject/iotex-core/v2/action/protocol"
"github.com/iotexproject/iotex-core/v2/blockchain/block"
"github.com/iotexproject/iotex-core/v2/blockchain/blockdao"
"github.com/iotexproject/iotex-core/v2/blockchain/filedao"
"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
"github.com/iotexproject/iotex-core/v2/pkg/lifecycle"
"github.com/iotexproject/iotex-core/v2/pkg/log"
"github.com/iotexproject/iotex-core/v2/pkg/prometheustimer"
"github.com/iotexproject/iotex-core/v2/pkg/unit"
)
// const
const (
SigP256k1 = "secp256k1"
SigP256sm2 = "p256sm2"
)
var (
_blockMtc = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "iotex_block_metrics",
Help: "Block metrics.",
},
[]string{"type"},
)
// ErrInvalidTipHeight is the error returned when the block height is not valid
ErrInvalidTipHeight = errors.New("invalid tip height")
// ErrInvalidBlock is the error returned when the block is not valid
ErrInvalidBlock = errors.New("failed to validate the block")
// ErrActionNonce is the error when the nonce of the action is wrong
ErrActionNonce = errors.New("invalid action nonce")
// ErrInsufficientGas indicates the error of insufficient gas value for data storage
ErrInsufficientGas = errors.New("insufficient intrinsic gas value")
// ErrBalance indicates the error of balance
ErrBalance = errors.New("invalid balance")
)
func init() {
prometheus.MustRegister(_blockMtc)
}
type (
// Blockchain represents the blockchain data structure and hosts the APIs to access it
Blockchain interface {
lifecycle.StartStopper
// For exposing blockchain states
// BlockHeaderByHeight return block header by height
BlockHeaderByHeight(height uint64) (*block.Header, error)
// BlockFooterByHeight return block footer by height
BlockFooterByHeight(height uint64) (*block.Footer, error)
// ChainID returns the chain ID
ChainID() uint32
// EvmNetworkID returns the evm network ID
EvmNetworkID() uint32
// ChainAddress returns chain address on parent chain, the root chain return empty.
ChainAddress() string
// TipHash returns tip block's hash
TipHash() hash.Hash256
// TipHeight returns tip block's height
TipHeight() uint64
// Genesis returns the genesis
Genesis() genesis.Genesis
// Context returns current context
Context(context.Context) (context.Context, error)
// ContextAtHeight returns context at given height
ContextAtHeight(context.Context, uint64) (context.Context, error)
// For block operations
// MintNewBlock creates a new block with given actions
// Note: the coinbase transfer will be added to the given transfers when minting a new block
MintNewBlock(timestamp time.Time) (*block.Block, error)
// CommitBlock validates and appends a block to the chain
CommitBlock(blk *block.Block) error
// ValidateBlock validates a new block before adding it to the blockchain
ValidateBlock(*block.Block, ...BlockValidationOption) error
// AddSubscriber make you listen to every single produced block
AddSubscriber(BlockCreationSubscriber) error
// RemoveSubscriber make you listen to every single produced block
RemoveSubscriber(BlockCreationSubscriber) error
}
// BlockBuilderFactory is the factory interface of block builder
BlockBuilderFactory interface {
// NewBlockBuilder creates block builder
NewBlockBuilder(context.Context, func(action.Envelope) (*action.SealedEnvelope, error)) (*block.Builder, error)
}
// blockchain implements the Blockchain interface
blockchain struct {
mu sync.RWMutex // mutex to protect utk, tipHeight and tipHash
dao blockdao.BlockDAO
config Config
genesis genesis.Genesis
blockValidator block.Validator
lifecycle lifecycle.Lifecycle
clk clock.Clock
pubSubManager PubSubManager
timerFactory *prometheustimer.TimerFactory
// used by account-based model
bbf BlockBuilderFactory
}
)
// Productivity returns the map of the number of blocks produced per delegate in given epoch
func Productivity(bc Blockchain, startHeight uint64, endHeight uint64) (map[string]uint64, error) {
stats := make(map[string]uint64)
for i := startHeight; i <= endHeight; i++ {
header, err := bc.BlockHeaderByHeight(i)
if err != nil {
return nil, err
}
producer := header.ProducerAddress()
stats[producer]++
}
return stats, nil
}
// Option sets blockchain construction parameter
type Option func(*blockchain) error
// BlockValidatorOption sets block validator
func BlockValidatorOption(blockValidator block.Validator) Option {
return func(bc *blockchain) error {
bc.blockValidator = blockValidator
return nil
}
}
// ClockOption overrides the default clock
func ClockOption(clk clock.Clock) Option {
return func(bc *blockchain) error {
bc.clk = clk
return nil
}
}
type (
BlockValidationCfg struct {
skipSidecarValidation bool
}
BlockValidationOption func(*BlockValidationCfg)
)
func SkipSidecarValidationOption() BlockValidationOption {
return func(opts *BlockValidationCfg) {
opts.skipSidecarValidation = true
}
}
// NewBlockchain creates a new blockchain and DB instance
func NewBlockchain(cfg Config, g genesis.Genesis, dao blockdao.BlockDAO, bbf BlockBuilderFactory, opts ...Option) Blockchain {
// create the Blockchain
chain := &blockchain{
config: cfg,
genesis: g,
dao: dao,
bbf: bbf,
clk: clock.New(),
pubSubManager: NewPubSub(cfg.StreamingBlockBufferSize),
}
for _, opt := range opts {
if err := opt(chain); err != nil {
log.S().Panicf("Failed to execute blockchain creation option %p: %v", opt, err)
}
}
timerFactory, err := prometheustimer.New(
"iotex_blockchain_perf",
"Performance of blockchain module",
[]string{"topic", "chainID"},
[]string{"default", strconv.FormatUint(uint64(cfg.ID), 10)},
)
if err != nil {
log.L().Panic("Failed to generate prometheus timer factory.", zap.Error(err))
}
chain.timerFactory = timerFactory
if chain.dao == nil {
log.L().Panic("blockdao is nil")
}
chain.lifecycle.Add(chain.dao)
chain.lifecycle.Add(chain.pubSubManager)
return chain
}
func (bc *blockchain) ChainID() uint32 {
return atomic.LoadUint32(&bc.config.ID)
}
func (bc *blockchain) EvmNetworkID() uint32 {
return atomic.LoadUint32(&bc.config.EVMNetworkID)
}
func (bc *blockchain) ChainAddress() string {
return bc.config.Address
}
// Start starts the blockchain
func (bc *blockchain) Start(ctx context.Context) error {
bc.mu.Lock()
defer bc.mu.Unlock()
// pass registry to be used by state factory's initialization
ctx = protocol.WithFeatureWithHeightCtx(genesis.WithGenesisContext(
protocol.WithBlockchainCtx(
ctx,
protocol.BlockchainCtx{
ChainID: bc.ChainID(),
EvmNetworkID: bc.EvmNetworkID(),
},
), bc.genesis))
return bc.lifecycle.OnStart(ctx)
}
// Stop stops the blockchain.
func (bc *blockchain) Stop(ctx context.Context) error {
bc.mu.Lock()
defer bc.mu.Unlock()
return bc.lifecycle.OnStop(ctx)
}
func (bc *blockchain) BlockHeaderByHeight(height uint64) (*block.Header, error) {
return bc.dao.HeaderByHeight(height)
}
func (bc *blockchain) BlockFooterByHeight(height uint64) (*block.Footer, error) {
return bc.dao.FooterByHeight(height)
}
// TipHash returns tip block's hash
func (bc *blockchain) TipHash() hash.Hash256 {
tipHeight, err := bc.dao.Height()
if err != nil {
return hash.ZeroHash256
}
tipHash, err := bc.dao.GetBlockHash(tipHeight)
if err != nil {
return hash.ZeroHash256
}
return tipHash
}
// TipHeight returns tip block's height
func (bc *blockchain) TipHeight() uint64 {
tipHeight, err := bc.dao.Height()
if err != nil {
log.L().Panic("failed to get tip height", zap.Error(err))
}
return tipHeight
}
// ValidateBlock validates a new block before adding it to the blockchain
func (bc *blockchain) ValidateBlock(blk *block.Block, opts ...BlockValidationOption) error {
bc.mu.RLock()
defer bc.mu.RUnlock()
timer := bc.timerFactory.NewTimer("ValidateBlock")
defer timer.End()
if blk == nil {
return ErrInvalidBlock
}
tipHeight, err := bc.dao.Height()
if err != nil {
return err
}
tip, err := bc.tipInfo(tipHeight)
if err != nil {
return err
}
// verify new block has height incremented by 1
if blk.Height() != 0 && blk.Height() != tip.Height+1 {
return errors.Wrapf(
ErrInvalidTipHeight,
"wrong block height %d, expecting %d",
blk.Height(),
tip.Height+1,
)
}
// verify new block has correctly linked to current tip
if blk.PrevHash() != tip.Hash {
blk.HeaderLogger(log.L()).Error("Previous block hash doesn't match.",
log.Hex("expectedBlockHash", tip.Hash[:]))
return errors.Wrapf(
ErrInvalidBlock,
"wrong prev hash %x, expecting %x",
blk.PrevHash(),
tip.Hash,
)
}
// verify EIP1559 header (baseFee adjustment)
if blk.Header.BaseFee() != nil {
if err = protocol.VerifyEIP1559Header(bc.genesis.Blockchain, tip, &blk.Header); err != nil {
return errors.Wrap(err, "failed to verify EIP1559 header (baseFee adjustment)")
}
}
if !blk.Header.VerifySignature() {
return errors.Errorf("failed to verify block's signature with public key: %x", blk.PublicKey())
}
if err := blk.VerifyTxRoot(); err != nil {
return err
}
producerAddr := blk.PublicKey().Address()
if producerAddr == nil {
return errors.New("failed to get address")
}
ctx, err := bc.context(context.Background(), tipHeight)
if err != nil {
return err
}
cfg := BlockValidationCfg{}
for _, opt := range opts {
opt(&cfg)
}
ctx = protocol.WithBlockCtx(ctx,
protocol.BlockCtx{
BlockHeight: blk.Height(),
BlockTimeStamp: blk.Timestamp(),
GasLimit: bc.genesis.BlockGasLimitByHeight(blk.Height()),
Producer: producerAddr,
BaseFee: blk.BaseFee(),
ExcessBlobGas: blk.ExcessBlobGas(),
SkipSidecarValidation: cfg.skipSidecarValidation,
},
)
ctx = protocol.WithFeatureCtx(ctx)
if bc.blockValidator == nil {
return nil
}
return bc.blockValidator.Validate(ctx, blk)
}
func (bc *blockchain) Context(ctx context.Context) (context.Context, error) {
bc.mu.RLock()
defer bc.mu.RUnlock()
tipHeight, err := bc.dao.Height()
if err != nil {
return nil, err
}
return bc.context(ctx, tipHeight)
}
func (bc *blockchain) ContextAtHeight(ctx context.Context, height uint64) (context.Context, error) {
bc.mu.RLock()
defer bc.mu.RUnlock()
return bc.context(ctx, height)
}
func (bc *blockchain) contextWithBlock(ctx context.Context, producer address.Address, height uint64, timestamp time.Time, baseFee *big.Int, blobgas uint64) context.Context {
return protocol.WithBlockCtx(
ctx,
protocol.BlockCtx{
BlockHeight: height,
BlockTimeStamp: timestamp,
Producer: producer,
GasLimit: bc.genesis.BlockGasLimitByHeight(height),
BaseFee: baseFee,
ExcessBlobGas: blobgas,
})
}
func (bc *blockchain) context(ctx context.Context, height uint64) (context.Context, error) {
tip, err := bc.tipInfo(height)
if err != nil {
return nil, err
}
ctx = genesis.WithGenesisContext(
protocol.WithBlockchainCtx(
ctx,
protocol.BlockchainCtx{
Tip: *tip,
ChainID: bc.ChainID(),
EvmNetworkID: bc.EvmNetworkID(),
},
),
bc.genesis,
)
return protocol.WithFeatureWithHeightCtx(ctx), nil
}
func (bc *blockchain) MintNewBlock(timestamp time.Time) (*block.Block, error) {
bc.mu.RLock()
defer bc.mu.RUnlock()
mintNewBlockTimer := bc.timerFactory.NewTimer("MintNewBlock")
defer mintNewBlockTimer.End()
tipHeight, err := bc.dao.Height()
if err != nil {
return nil, err
}
newblockHeight := tipHeight + 1
ctx, err := bc.context(context.Background(), tipHeight)
if err != nil {
return nil, err
}
tip := protocol.MustGetBlockchainCtx(ctx).Tip
ctx = bc.contextWithBlock(ctx, bc.config.ProducerAddress(), newblockHeight, timestamp, protocol.CalcBaseFee(genesis.MustExtractGenesisContext(ctx).Blockchain, &tip), protocol.CalcExcessBlobGas(tip.ExcessBlobGas, tip.BlobGasUsed))
ctx = protocol.WithFeatureCtx(ctx)
// run execution and update state trie root hash
minterPrivateKey := bc.config.ProducerPrivateKey()
blockBuilder, err := bc.bbf.NewBlockBuilder(
ctx,
func(elp action.Envelope) (*action.SealedEnvelope, error) {
return action.Sign(elp, minterPrivateKey)
},
)
if err != nil {
return nil, errors.Wrapf(err, "failed to create block builder at new block height %d", newblockHeight)
}
blk, err := blockBuilder.SignAndBuild(minterPrivateKey)
if err != nil {
return nil, errors.Wrapf(err, "failed to create block")
}
_blockMtc.WithLabelValues("MintGas").Set(float64(blk.GasUsed()))
_blockMtc.WithLabelValues("MintActions").Set(float64(len(blk.Actions)))
return &blk, nil
}
// CommitBlock validates and appends a block to the chain
func (bc *blockchain) CommitBlock(blk *block.Block) error {
bc.mu.Lock()
defer bc.mu.Unlock()
timer := bc.timerFactory.NewTimer("CommitBlock")
defer timer.End()
return bc.commitBlock(blk)
}
func (bc *blockchain) AddSubscriber(s BlockCreationSubscriber) error {
log.L().Info("Add a subscriber.")
if s == nil {
return errors.New("subscriber could not be nil")
}
return bc.pubSubManager.AddBlockListener(s)
}
func (bc *blockchain) RemoveSubscriber(s BlockCreationSubscriber) error {
return bc.pubSubManager.RemoveBlockListener(s)
}
//======================================
// internal functions
//=====================================
func (bc *blockchain) Genesis() genesis.Genesis {
return bc.genesis
}
//======================================
// private functions
//=====================================
func (bc *blockchain) tipInfo(tipHeight uint64) (*protocol.TipInfo, error) {
if tipHeight == 0 {
return &protocol.TipInfo{
Height: 0,
Hash: bc.genesis.Hash(),
Timestamp: time.Unix(bc.genesis.Timestamp, 0),
}, nil
}
header, err := bc.dao.HeaderByHeight(tipHeight)
if err != nil {
return nil, err
}
return &protocol.TipInfo{
Height: tipHeight,
GasUsed: header.GasUsed(),
Hash: header.HashBlock(),
Timestamp: header.Timestamp(),
BaseFee: header.BaseFee(),
BlobGasUsed: header.BlobGasUsed(),
ExcessBlobGas: header.ExcessBlobGas(),
}, nil
}
// commitBlock commits a block to the chain
func (bc *blockchain) commitBlock(blk *block.Block) error {
tipHeight, err := bc.dao.Height()
if err != nil {
return err
}
ctx, err := bc.context(context.Background(), tipHeight)
if err != nil {
return err
}
ctx = bc.contextWithBlock(ctx, blk.PublicKey().Address(), blk.Height(), blk.Timestamp(), blk.BaseFee(), blk.ExcessBlobGas())
ctx = protocol.WithFeatureCtx(ctx)
// write block into DB
putTimer := bc.timerFactory.NewTimer("putBlock")
err = bc.dao.PutBlock(ctx, blk)
putTimer.End()
switch errors.Cause(err) {
case filedao.ErrAlreadyExist, blockdao.ErrAlreadyExist:
return nil
case nil:
// do nothing
default:
return err
}
blkHash := blk.HashBlock()
if blk.Height()%100 == 0 {
blk.HeaderLogger(log.L()).Info("Committed a block.", log.Hex("tipHash", blkHash[:]))
}
_blockMtc.WithLabelValues("numActions").Set(float64(len(blk.Actions)))
if blk.BaseFee() != nil {
basefeeQev := new(big.Int).Div(blk.BaseFee(), big.NewInt(unit.Qev))
_blockMtc.WithLabelValues("baseFee").Set(float64(basefeeQev.Int64()))
}
_blockMtc.WithLabelValues("excessBlobGas").Set(float64(blk.ExcessBlobGas()))
_blockMtc.WithLabelValues("blobGasUsed").Set(float64(blk.BlobGasUsed()))
// emit block to all block subscribers
bc.emitToSubscribers(blk)
return nil
}
func (bc *blockchain) emitToSubscribers(blk *block.Block) {
if bc.pubSubManager == nil {
return
}
bc.pubSubManager.SendBlockToSubscribers(blk)
}