-
Notifications
You must be signed in to change notification settings - Fork 790
/
Copy pathindex.ts
1604 lines (1440 loc) · 54.7 KB
/
index.ts
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
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { debug as createDebugLogger } from 'debug'
import Semaphore from 'semaphore-async-await'
import { Address, BN, rlp } from 'ethereumjs-util'
import { Block, BlockData, BlockHeader } from '@ethereumjs/block'
import Ethash from '@ethereumjs/ethash'
import Common, { Chain, ConsensusAlgorithm, ConsensusType, Hardfork } from '@ethereumjs/common'
import { DBManager } from './db/manager'
import { DBOp, DBSetBlockOrHeader, DBSetTD, DBSetHashToNumber, DBSaveLookups } from './db/helpers'
import { DBTarget } from './db/operation'
import {
CliqueSignerState,
CliqueLatestSignerStates,
CliqueVote,
CliqueLatestVotes,
CliqueBlockSigner,
CliqueLatestBlockSigners,
CLIQUE_NONCE_AUTH,
CLIQUE_NONCE_DROP,
} from './clique'
const debug = createDebugLogger('blockchain:clique')
// eslint-disable-next-line implicit-dependencies/no-implicit
import type { LevelUp } from 'levelup'
const level = require('level-mem')
type OnBlock = (block: Block, reorg: boolean) => Promise<void> | void
export interface BlockchainInterface {
/**
* Adds a block to the blockchain.
*
* @param block - The block to be added to the blockchain.
*/
putBlock(block: Block): Promise<void>
/**
* Deletes a block from the blockchain. All child blocks in the chain are
* deleted and any encountered heads are set to the parent block.
*
* @param blockHash - The hash of the block to be deleted
*/
delBlock(blockHash: Buffer): Promise<void>
/**
* Returns a block by its hash or number.
*/
getBlock(blockId: Buffer | number | BN): Promise<Block | null>
/**
* Iterates through blocks starting at the specified iterator head and calls
* the onBlock function on each block.
*
* @param name - Name of the state root head
* @param onBlock - Function called on each block with params (block: Block,
* reorg: boolean)
*/
iterator(name: string, onBlock: OnBlock): Promise<void | number>
}
/**
* This are the options that the Blockchain constructor can receive.
*/
export interface BlockchainOptions {
/**
* Specify the chain and hardfork by passing a {@link Common} instance.
*
* If not provided this defaults to chain `mainnet` and hardfork `chainstart`
*
*/
common?: Common
/**
* Set the HF to the fork determined by the head block and update on head updates.
*
* Note: for HFs where the transition is also determined by a total difficulty
* threshold (merge HF) the calculated TD is additionally taken into account
* for HF determination.
*
* Default: `false` (HF is set to whatever default HF is set by the {@link Common} instance)
*/
hardforkByHeadBlockNumber?: boolean
/**
* Database to store blocks and metadata.
* Should be an `abstract-leveldown` compliant store
* wrapped with `encoding-down`.
* For example:
* `levelup(encode(leveldown('./db1')))`
* or use the `level` convenience package:
* `level('./db1')`
*/
db?: LevelUp
/**
* This flags indicates if a block should be validated along the consensus algorithm
* or protocol used by the chain, e.g. by verifying the PoW on the block.
*
* Supported consensus types and algorithms (taken from the `Common` instance):
* - 'pow' with 'ethash' algorithm (validates the proof-of-work)
* - 'poa' with 'clique' algorithm (verifies the block signatures)
* Default: `true`.
*/
validateConsensus?: boolean
/**
* This flag indicates if protocol-given consistency checks on
* block headers and included uncles and transactions should be performed,
* see Block#validate for details.
*
*/
validateBlocks?: boolean
/**
* The blockchain only initializes succesfully if it has a genesis block. If
* there is no block available in the DB and a `genesisBlock` is provided,
* then the provided `genesisBlock` will be used as genesis. If no block is
* present in the DB and no block is provided, then the genesis block as
* provided from the `common` will be used.
*/
genesisBlock?: Block
}
/**
* This class stores and interacts with blocks.
*/
export default class Blockchain implements BlockchainInterface {
db: LevelUp
dbManager: DBManager
private _genesis?: Buffer // the genesis hash of this blockchain
// The following two heads and the heads stored within the `_heads` always point
// to a hash in the canonical chain and never to a stale hash.
// With the exception of `_headHeaderHash` this does not necessarily need to be
// the hash with the highest total difficulty.
private _headBlockHash?: Buffer // the hash of the current head block
private _headHeaderHash?: Buffer // the hash of the current head header
// A Map which stores the head of each key (for instance the "vm" key) which is
// updated along a {@link Blockchain.iterator} method run and can be used to (re)run
// non-verified blocks (for instance in the VM).
private _heads: { [key: string]: Buffer }
public initPromise: Promise<void>
private _lock: Semaphore
private _common: Common
private _hardforkByHeadBlockNumber: boolean
private readonly _validateConsensus: boolean
private readonly _validateBlocks: boolean
_ethash?: Ethash
/**
* Keep signer history data (signer states and votes)
* for all block numbers >= HEAD_BLOCK - CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
*
* This defines a limit for reorgs on PoA clique chains.
*/
private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 100
/**
* List with the latest signer states checkpointed on blocks where
* a change (added new or removed a signer) occurred.
*
* Format:
* [ [BLOCK_NUMBER_1, [SIGNER1, SIGNER 2,]], [BLOCK_NUMBER2, [SIGNER1, SIGNER3]], ...]
*
* The top element from the array represents the list of current signers.
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
*
* Always keep at least one item on the stack.
*/
private _cliqueLatestSignerStates: CliqueLatestSignerStates = []
/**
* List with the latest signer votes.
*
* Format:
* [ [BLOCK_NUMBER_1, [SIGNER, BENEFICIARY, AUTH]], [BLOCK_NUMBER_1, [SIGNER, BENEFICIARY, AUTH]] ]
* where AUTH = CLIQUE_NONCE_AUTH | CLIQUE_NONCE_DROP
*
* For votes all elements here must be taken into account with a
* block number >= LAST_EPOCH_BLOCK
* (nevertheless keep entries with blocks before EPOCH_BLOCK in case a reorg happens
* during an epoch change)
*
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
*/
private _cliqueLatestVotes: CliqueLatestVotes = []
/**
* List of signers for the last consecutive {@link Blockchain.cliqueSignerLimit} blocks.
* Kept as a snapshot for quickly checking for "recently signed" error.
* Format: [ [BLOCK_NUMBER, SIGNER_ADDRESS], ...]
*
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
*/
private _cliqueLatestBlockSigners: CliqueLatestBlockSigners = []
/**
* Safe creation of a new Blockchain object awaiting the initialization function,
* encouraged method to use when creating a blockchain object.
*
* @param opts Constructor options, see {@link BlockchainOptions}
*/
public static async create(opts: BlockchainOptions = {}) {
const blockchain = new Blockchain(opts)
await blockchain.initPromise!.catch((e) => {
throw e
})
return blockchain
}
/**
* Creates a blockchain from a list of block objects,
* objects must be readable by {@link Block.fromBlockData}
*
* @param blockData List of block objects
* @param opts Constructor options, see {@link BlockchainOptions}
*/
public static async fromBlocksData(blocksData: BlockData[], opts: BlockchainOptions = {}) {
const blockchain = await Blockchain.create(opts)
for (const blockData of blocksData) {
const block = Block.fromBlockData(blockData, {
common: blockchain._common,
hardforkByBlockNumber: true,
})
await blockchain.putBlock(block)
}
return blockchain
}
/**
* Creates new Blockchain object
*
* @deprecated - The direct usage of this constructor is discouraged since
* non-finalized async initialization might lead to side effects. Please
* use the async {@link Blockchain.create} constructor instead (same API).
*
* @param opts - An object with the options that this constructor takes. See
* {@link BlockchainOptions}.
*/
constructor(opts: BlockchainOptions = {}) {
// Throw on chain or hardfork options removed in latest major release to
// prevent implicit chain setup on a wrong chain
if ('chain' in opts || 'hardfork' in opts) {
throw new Error('Chain/hardfork options are not allowed any more on initialization')
}
if (opts.common) {
this._common = opts.common
} else {
const DEFAULT_CHAIN = Chain.Mainnet
const DEFAULT_HARDFORK = Hardfork.Chainstart
this._common = new Common({
chain: DEFAULT_CHAIN,
hardfork: DEFAULT_HARDFORK,
})
}
this._hardforkByHeadBlockNumber = opts.hardforkByHeadBlockNumber ?? false
this._validateConsensus = opts.validateConsensus ?? true
this._validateBlocks = opts.validateBlocks ?? true
this.db = opts.db ? opts.db : level()
this.dbManager = new DBManager(this.db, this._common)
if (this._validateConsensus) {
if (this._common.consensusType() === ConsensusType.ProofOfWork) {
if (this._common.consensusAlgorithm() !== ConsensusAlgorithm.Ethash) {
throw new Error('consensus validation only supported for pow ethash algorithm')
} else {
this._ethash = new Ethash(this.db)
}
}
if (this._common.consensusType() === ConsensusType.ProofOfAuthority) {
if (this._common.consensusAlgorithm() !== ConsensusAlgorithm.Clique) {
throw new Error(
'consensus (signature) validation only supported for poa clique algorithm'
)
}
}
}
this._heads = {}
this._lock = new Semaphore(1)
if (opts.genesisBlock && !opts.genesisBlock.isGenesis()) {
throw 'supplied block is not a genesis block'
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.initPromise = this._init(opts.genesisBlock)
}
/**
* Returns an object with metadata about the Blockchain. It's defined for
* backwards compatibility.
*/
get meta() {
return {
rawHead: this._headHeaderHash,
heads: this._heads,
genesis: this._genesis,
}
}
/**
* Returns a deep copy of this {@link Blockchain} instance.
*
* Note: this does not make a copy of the underlying db
* since it is unknown if the source is on disk or in memory.
* This should not be a significant issue in most usage since
* the queries will only reflect the instance's known data.
* If you would like this copied blockchain to use another db
* set the {@link db} of this returned instance to a copy of
* the original.
*/
copy(): Blockchain {
const copiedBlockchain = Object.create(
Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this)
)
copiedBlockchain._common = this._common.copy()
return copiedBlockchain
}
/**
* This method is called in the constructor and either sets up the DB or reads
* values from the DB and makes these available to the consumers of
* Blockchain.
*
* @hidden
*/
private async _init(genesisBlock?: Block): Promise<void> {
let dbGenesisBlock
try {
const genesisHash = await this.dbManager.numberToHash(new BN(0))
dbGenesisBlock = await this.dbManager.getBlock(genesisHash)
} catch (error: any) {
if (error.type !== 'NotFoundError') {
throw error
}
}
if (!genesisBlock) {
const common = this._common.copy()
common.setHardforkByBlockNumber(0)
genesisBlock = Block.genesis({}, { common })
}
// If the DB has a genesis block, then verify that the genesis block in the
// DB is indeed the Genesis block generated or assigned.
if (dbGenesisBlock && !genesisBlock.hash().equals(dbGenesisBlock.hash())) {
throw new Error(
'The genesis block in the DB has a different hash than the provided genesis block.'
)
}
const genesisHash = genesisBlock.hash()
if (!dbGenesisBlock) {
// If there is no genesis block put the genesis block in the DB.
// For that TD, the BlockOrHeader, and the Lookups have to be saved.
const dbOps: DBOp[] = []
dbOps.push(DBSetTD(genesisBlock.header.difficulty.clone(), new BN(0), genesisHash))
DBSetBlockOrHeader(genesisBlock).map((op) => dbOps.push(op))
DBSaveLookups(genesisHash, new BN(0)).map((op) => dbOps.push(op))
await this.dbManager.batch(dbOps)
if (this._common.consensusAlgorithm() === ConsensusAlgorithm.Clique) {
await this.cliqueSaveGenesisSigners(genesisBlock)
}
}
// Clique: read current signer states, signer votes, and block signers
if (this._common.consensusAlgorithm() === ConsensusAlgorithm.Clique) {
this._cliqueLatestSignerStates = await this.dbManager.getCliqueLatestSignerStates()
this._cliqueLatestVotes = await this.dbManager.getCliqueLatestVotes()
this._cliqueLatestBlockSigners = await this.dbManager.getCliqueLatestBlockSigners()
}
// At this point, we can safely set genesisHash as the _genesis hash in this
// object: it is either the one we put in the DB, or it is equal to the one
// which we read from the DB.
this._genesis = genesisHash
// load verified iterator heads
try {
const heads = await this.dbManager.getHeads()
this._heads = heads
} catch (error: any) {
if (error.type !== 'NotFoundError') {
throw error
}
this._heads = {}
}
// load headerchain head
try {
const hash = await this.dbManager.getHeadHeader()
this._headHeaderHash = hash
} catch (error: any) {
if (error.type !== 'NotFoundError') {
throw error
}
this._headHeaderHash = genesisHash
}
// load blockchain head
try {
const hash = await this.dbManager.getHeadBlock()
this._headBlockHash = hash
} catch (error: any) {
if (error.type !== 'NotFoundError') {
throw error
}
this._headBlockHash = genesisHash
}
if (this._hardforkByHeadBlockNumber) {
const latestHeader = await this._getHeader(this._headHeaderHash)
const td = await this.getTotalDifficulty(this._headHeaderHash)
this._common.setHardforkByBlockNumber(latestHeader.number, td)
}
}
/**
* Perform the `action` function after we have initialized this module and
* have acquired a lock
* @param action - the action function to run after initializing and acquiring
* a lock
* @hidden
*/
private async initAndLock<T>(action: () => Promise<T>): Promise<T> {
await this.initPromise
return await this.runWithLock(action)
}
/**
* Run a function after acquiring a lock. It is implied that we have already
* initialized the module (or we are calling this from the init function, like
* `_setCanonicalGenesisBlock`)
* @param action - function to run after acquiring a lock
* @hidden
*/
private async runWithLock<T>(action: () => Promise<T>): Promise<T> {
try {
await this._lock.acquire()
const value = await action()
return value
} finally {
this._lock.release()
}
}
private _requireClique() {
if (this._common.consensusAlgorithm() !== ConsensusAlgorithm.Clique) {
throw new Error('Function call only supported for clique PoA networks')
}
}
/**
* Checks if signer was recently signed.
* Returns true if signed too recently: more than once per {@link Blockchain.cliqueSignerLimit} consecutive blocks.
* @param header BlockHeader
* @hidden
*/
private cliqueCheckRecentlySigned(header: BlockHeader): boolean {
if (header.isGenesis() || header.number.eqn(1)) {
// skip genesis, first block
return false
}
const limit = this.cliqueSignerLimit()
// construct recent block signers list with this block
let signers = this._cliqueLatestBlockSigners
signers = signers.slice(signers.length < limit ? 0 : 1)
signers.push([header.number, header.cliqueSigner()])
const seen = signers.filter((s) => s[1].equals(header.cliqueSigner())).length
return seen > 1
}
/**
* Save genesis signers to db
* @param genesisBlock genesis block
* @hidden
*/
private async cliqueSaveGenesisSigners(genesisBlock: Block) {
const genesisSignerState: CliqueSignerState = [
new BN(0),
genesisBlock.header.cliqueEpochTransitionSigners(),
]
await this.cliqueUpdateSignerStates(genesisSignerState)
debug(`[Block 0] Genesis block -> update signer states`)
await this.cliqueUpdateVotes()
}
/**
* Save signer state to db
* @param signerState
* @hidden
*/
private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) {
const dbOps: DBOp[] = []
if (signerState) {
this._cliqueLatestSignerStates.push(signerState)
}
// trim to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
const blockSigners = this._cliqueLatestBlockSigners
const lastBlockNumber = blockSigners[blockSigners.length - 1]?.[0]
if (lastBlockNumber) {
const blockLimit = lastBlockNumber.subn(limit)
const states = this._cliqueLatestSignerStates
const lastItem = states[states.length - 1]
this._cliqueLatestSignerStates = states.filter((state) => state[0].gte(blockLimit))
if (this._cliqueLatestSignerStates.length === 0) {
// always keep at least one item on the stack
this._cliqueLatestSignerStates.push(lastItem)
}
}
// save to db
const formatted = this._cliqueLatestSignerStates.map((state) => [
state[0].toArrayLike(Buffer),
state[1].map((a) => a.toBuffer()),
])
dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(formatted)))
await this.dbManager.batch(dbOps)
// Output active signers for debugging purposes
let i = 0
for (const signer of this.cliqueActiveSigners()) {
debug(`Clique signer [${i}]: ${signer}`)
i++
}
}
/**
* Update clique votes and save to db
* @param header BlockHeader
* @hidden
*/
private async cliqueUpdateVotes(header?: BlockHeader) {
// Block contains a vote on a new signer
if (header && !header.coinbase.isZero()) {
const signer = header.cliqueSigner()
const beneficiary = header.coinbase
const nonce = header.nonce
const latestVote: CliqueVote = [header.number, [signer, beneficiary, nonce]]
// Do two rounds here, one to execute on a potential previously reached consensus
// on the newly touched beneficiary, one with the added new vote
for (let round = 1; round <= 2; round++) {
// See if there is a new majority consensus to update the signer list
const lastEpochBlockNumber = header.number.sub(
header.number.mod(new BN(this._common.consensusConfig().epoch))
)
const limit = this.cliqueSignerLimit()
let activeSigners = this.cliqueActiveSigners()
let consensus = false
// AUTH vote analysis
let votes = this._cliqueLatestVotes.filter((vote) => {
return (
vote[0].gte(lastEpochBlockNumber) &&
!vote[1][0].equals(signer) &&
vote[1][1].equals(beneficiary) &&
vote[1][2].equals(CLIQUE_NONCE_AUTH)
)
})
const beneficiaryVotesAUTH: Address[] = []
for (const vote of votes) {
const num = beneficiaryVotesAUTH.filter((voteCMP) => {
return voteCMP.equals(vote[1][0])
}).length
if (num === 0) {
beneficiaryVotesAUTH.push(vote[1][0])
}
}
let numBeneficiaryVotesAUTH = beneficiaryVotesAUTH.length
if (round === 2 && nonce.equals(CLIQUE_NONCE_AUTH)) {
numBeneficiaryVotesAUTH += 1
}
// Majority consensus
if (numBeneficiaryVotesAUTH >= limit) {
consensus = true
// Authorize new signer
activeSigners.push(beneficiary)
activeSigners.sort((a, b) => {
// Sort by buffer size
return a.toBuffer().compare(b.toBuffer())
})
// Discard votes for added signer
this._cliqueLatestVotes = this._cliqueLatestVotes.filter(
(vote) => !vote[1][1].equals(beneficiary)
)
debug(`[Block ${header.number}] Clique majority consensus (AUTH ${beneficiary})`)
}
// DROP vote
votes = this._cliqueLatestVotes.filter((vote) => {
return (
vote[0].gte(lastEpochBlockNumber) &&
!vote[1][0].equals(signer) &&
vote[1][1].equals(beneficiary) &&
vote[1][2].equals(CLIQUE_NONCE_DROP)
)
})
const beneficiaryVotesDROP: Address[] = []
for (const vote of votes) {
const num = beneficiaryVotesDROP.filter((voteCMP) => {
return voteCMP.equals(vote[1][0])
}).length
if (num === 0) {
beneficiaryVotesDROP.push(vote[1][0])
}
}
let numBeneficiaryVotesDROP = beneficiaryVotesDROP.length
if (round === 2 && nonce.equals(CLIQUE_NONCE_DROP)) {
numBeneficiaryVotesDROP += 1
}
// Majority consensus
if (numBeneficiaryVotesDROP >= limit) {
consensus = true
// Drop signer
activeSigners = activeSigners.filter((signer) => !signer.equals(beneficiary))
this._cliqueLatestVotes = this._cliqueLatestVotes.filter(
// Discard votes from removed signer and for removed signer
(vote) => !vote[1][0].equals(beneficiary) && !vote[1][1].equals(beneficiary)
)
debug(`[Block ${header.number}] Clique majority consensus (DROP ${beneficiary})`)
}
if (round === 1) {
// Always add the latest vote to the history no matter if already voted
// the same vote or not
this._cliqueLatestVotes.push(latestVote)
debug(
`[Block ${header.number}] New clique vote: ${signer} -> ${beneficiary} ${
nonce.equals(CLIQUE_NONCE_AUTH) ? 'AUTH' : 'DROP'
}`
)
}
if (consensus) {
if (round === 1) {
debug(
`[Block ${header.number}] Clique majority consensus on existing votes -> update signer states`
)
} else {
debug(
`[Block ${header.number}] Clique majority consensus on new vote -> update signer states`
)
}
const newSignerState: CliqueSignerState = [header.number, activeSigners]
await this.cliqueUpdateSignerStates(newSignerState)
return
}
}
}
// trim to lastEpochBlockNumber - CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
const blockSigners = this._cliqueLatestBlockSigners
const lastBlockNumber = blockSigners[blockSigners.length - 1]?.[0]
if (lastBlockNumber) {
const lastEpochBlockNumber = lastBlockNumber.sub(
lastBlockNumber.mod(new BN(this._common.consensusConfig().epoch))
)
const blockLimit = lastEpochBlockNumber.subn(limit)
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((state) => state[0].gte(blockLimit))
}
// save votes to db
const dbOps: DBOp[] = []
const formatted = this._cliqueLatestVotes.map((v) => [
v[0].toArrayLike(Buffer),
[v[1][0].toBuffer(), v[1][1].toBuffer(), v[1][2]],
])
dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(formatted)))
await this.dbManager.batch(dbOps)
}
/**
* Update snapshot of latest clique block signers.
* Used for checking for 'recently signed' error.
* Length trimmed to {@link Blockchain.cliqueSignerLimit}.
* @param header BlockHeader
* @hidden
*/
private async cliqueUpdateLatestBlockSigners(header?: BlockHeader) {
const dbOps: DBOp[] = []
if (header) {
if (header.isGenesis()) {
return
}
// add this block's signer
const signer: CliqueBlockSigner = [header.number, header.cliqueSigner()]
this._cliqueLatestBlockSigners.push(signer)
// trim length to `this.cliqueSignerLimit()`
const length = this._cliqueLatestBlockSigners.length
const limit = this.cliqueSignerLimit()
if (length > limit) {
this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice(
length - limit,
length
)
}
}
// save to db
const formatted = this._cliqueLatestBlockSigners.map((b) => [
b[0].toArrayLike(Buffer),
b[1].toBuffer(),
])
dbOps.push(DBOp.set(DBTarget.CliqueBlockSigners, rlp.encode(formatted)))
await this.dbManager.batch(dbOps)
}
/**
* Returns a list with the current block signers
* (only clique PoA, throws otherwise)
*/
public cliqueActiveSigners(): Address[] {
this._requireClique()
const signers = this._cliqueLatestSignerStates
if (signers.length === 0) {
return []
}
return [...signers[signers.length - 1][1]]
}
/**
* Number of consecutive blocks out of which a signer may only sign one.
* Defined as `Math.floor(SIGNER_COUNT / 2) + 1` to enforce majority consensus.
* signer count -> signer limit:
* 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3, ...
* @hidden
*/
private cliqueSignerLimit() {
return Math.floor(this.cliqueActiveSigners().length / 2) + 1
}
/**
* Returns the specified iterator head.
*
* This function replaces the old {@link Blockchain.getHead} method. Note that
* the function deviates from the old behavior and returns the
* genesis hash instead of the current head block if an iterator
* has not been run. This matches the behavior of {@link Blockchain.iterator}.
*
* @param name - Optional name of the iterator head (default: 'vm')
*/
async getIteratorHead(name = 'vm'): Promise<Block> {
return await this.initAndLock<Block>(async () => {
// if the head is not found return the genesis hash
const hash = this._heads[name] || this._genesis
if (!hash) {
throw new Error('No head found.')
}
const block = await this._getBlock(hash)
return block
})
}
/**
* Returns the specified iterator head.
*
* @param name - Optional name of the iterator head (default: 'vm')
*
* @deprecated use {@link Blockchain.getIteratorHead} instead.
* Note that {@link Blockchain.getIteratorHead} doesn't return
* the `headHeader` but the genesis hash as an initial iterator
* head value (now matching the behavior of {@link Blockchain.iterator}
* on a first run)
*/
async getHead(name = 'vm'): Promise<Block> {
return await this.initAndLock<Block>(async () => {
// if the head is not found return the headHeader
const hash = this._heads[name] || this._headBlockHash
if (!hash) {
throw new Error('No head found.')
}
const block = await this._getBlock(hash)
return block
})
}
/**
* Returns the latest header in the canonical chain.
*/
async getLatestHeader(): Promise<BlockHeader> {
return await this.initAndLock<BlockHeader>(async () => {
if (!this._headHeaderHash) {
throw new Error('No head header set')
}
const block = await this._getBlock(this._headHeaderHash)
return block.header
})
}
/**
* Returns the latest full block in the canonical chain.
*/
async getLatestBlock(): Promise<Block> {
return this.initAndLock<Block>(async () => {
if (!this._headBlockHash) {
throw new Error('No head block set')
}
const block = this._getBlock(this._headBlockHash)
return block
})
}
/**
* Adds blocks to the blockchain.
*
* If an invalid block is met the function will throw, blocks before will
* nevertheless remain in the DB. If any of the saved blocks has a higher
* total difficulty than the current max total difficulty the canonical
* chain is rebuilt and any stale heads/hashes are overwritten.
* @param blocks - The blocks to be added to the blockchain
*/
async putBlocks(blocks: Block[]) {
await this.initPromise
for (let i = 0; i < blocks.length; i++) {
await this.putBlock(blocks[i])
}
}
/**
* Adds a block to the blockchain.
*
* If the block is valid and has a higher total difficulty than the current
* max total difficulty, the canonical chain is rebuilt and any stale
* heads/hashes are overwritten.
* @param block - The block to be added to the blockchain
*/
async putBlock(block: Block) {
await this.initPromise
await this._putBlockOrHeader(block)
}
/**
* Adds many headers to the blockchain.
*
* If an invalid header is met the function will throw, headers before will
* nevertheless remain in the DB. If any of the saved headers has a higher
* total difficulty than the current max total difficulty the canonical
* chain is rebuilt and any stale heads/hashes are overwritten.
* @param headers - The headers to be added to the blockchain
*/
async putHeaders(headers: Array<any>) {
await this.initPromise
for (let i = 0; i < headers.length; i++) {
await this.putHeader(headers[i])
}
}
/**
* Adds a header to the blockchain.
*
* If this header is valid and it has a higher total difficulty than the current
* max total difficulty, the canonical chain is rebuilt and any stale
* heads/hashes are overwritten.
* @param header - The header to be added to the blockchain
*/
async putHeader(header: BlockHeader) {
await this.initPromise
await this._putBlockOrHeader(header)
}
/**
* Entrypoint for putting any block or block header. Verifies this block,
* checks the total TD: if this TD is higher than the current highest TD, we
* have thus found a new canonical block and have to rewrite the canonical
* chain. This also updates the head block hashes. If any of the older known
* canonical chains just became stale, then we also reset every _heads header
* which points to a stale header to the last verified header which was in the
* old canonical chain, but also in the new canonical chain. This thus rolls
* back these headers so that these can be updated to the "new" canonical
* header using the iterator method.
* @hidden
*/
private async _putBlockOrHeader(item: Block | BlockHeader) {
await this.runWithLock<void>(async () => {
const block =
item instanceof BlockHeader
? new Block(item, undefined, undefined, {
common: item._common,
})
: item
const isGenesis = block.isGenesis()
const isHeader = item instanceof BlockHeader
// we cannot overwrite the Genesis block after initializing the Blockchain
if (isGenesis) {
throw new Error('Cannot put a genesis block: create a new Blockchain')
}
const { header } = block
const blockHash = header.hash()
const blockNumber = header.number
const td = header.difficulty.clone()
const currentTd = { header: new BN(0), block: new BN(0) }
let dbOps: DBOp[] = []
if (!block._common.chainIdBN().eq(this._common.chainIdBN())) {
throw new Error('Chain mismatch while trying to put block or header')
}
if (this._validateBlocks && !isGenesis) {
// this calls into `getBlock`, which is why we cannot lock yet
await block.validate(this, isHeader)
}
if (this._validateConsensus) {
if (this._common.consensusAlgorithm() === ConsensusAlgorithm.Ethash) {
const valid = await this._ethash!.verifyPOW(block)
if (!valid) {
throw new Error('invalid POW')
}
}
if (this._common.consensusAlgorithm() === ConsensusAlgorithm.Clique) {
const valid = header.cliqueVerifySignature(this.cliqueActiveSigners())
if (!valid) {
throw new Error('invalid PoA block signature (clique)')
}
if (this.cliqueCheckRecentlySigned(header)) {
throw new Error('recently signed')
}
}
}
if (this._common.consensusAlgorithm() === ConsensusAlgorithm.Clique) {
// validate checkpoint signers towards active signers on epoch transition blocks
if (header.cliqueIsEpochTransition()) {
// note: keep votes on epoch transition blocks in case of reorgs.
// only active (non-stale) votes will counted (if vote.blockNumber >= lastEpochBlockNumber)
const checkpointSigners = header.cliqueEpochTransitionSigners()
const activeSigners = this.cliqueActiveSigners()
for (const [i, cSigner] of checkpointSigners.entries()) {
if (!activeSigners[i] || !activeSigners[i].equals(cSigner)) {
throw new Error(
`checkpoint signer not found in active signers list at index ${i}: ${cSigner}`
)
}
}
}
}
// set total difficulty in the current context scope
if (this._headHeaderHash) {
currentTd.header = await this.getTotalDifficulty(this._headHeaderHash)
}
if (this._headBlockHash) {
currentTd.block = await this.getTotalDifficulty(this._headBlockHash)
}
// calculate the total difficulty of the new block
let parentTd = new BN(0)
if (!block.isGenesis()) {
parentTd = await this.getTotalDifficulty(header.parentHash, blockNumber.subn(1))
}
td.iadd(parentTd)
// save total difficulty to the database
dbOps = dbOps.concat(DBSetTD(td, blockNumber, blockHash))
// save header/block to the database
dbOps = dbOps.concat(DBSetBlockOrHeader(block))
let ancientHeaderNumber: undefined | BN
// if total difficulty is higher than current, add it to canonical chain
if (
block.isGenesis() ||
(block._common.consensusType() !== ConsensusType.ProofOfStake && td.gt(currentTd.header)) ||
block._common.consensusType() === ConsensusType.ProofOfStake
) {
if (this._common.consensusAlgorithm() === ConsensusAlgorithm.Clique) {
ancientHeaderNumber = (await this._findAncient(header)).number
}
this._headHeaderHash = blockHash
if (item instanceof Block) {