@feelyourprotocol/blockchain 8141.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -0
- package/dist/cjs/blockchain.d.ts +372 -0
- package/dist/cjs/blockchain.d.ts.map +1 -0
- package/dist/cjs/blockchain.js +1105 -0
- package/dist/cjs/blockchain.js.map +1 -0
- package/dist/cjs/consensus/casper.d.ts +16 -0
- package/dist/cjs/consensus/casper.d.ts.map +1 -0
- package/dist/cjs/consensus/casper.js +28 -0
- package/dist/cjs/consensus/casper.js.map +1 -0
- package/dist/cjs/consensus/clique.d.ts +164 -0
- package/dist/cjs/consensus/clique.d.ts.map +1 -0
- package/dist/cjs/consensus/clique.js +520 -0
- package/dist/cjs/consensus/clique.js.map +1 -0
- package/dist/cjs/consensus/ethash.d.ts +29 -0
- package/dist/cjs/consensus/ethash.d.ts.map +1 -0
- package/dist/cjs/consensus/ethash.js +48 -0
- package/dist/cjs/consensus/ethash.js.map +1 -0
- package/dist/cjs/consensus/index.d.ts +5 -0
- package/dist/cjs/consensus/index.d.ts.map +1 -0
- package/dist/cjs/consensus/index.js +10 -0
- package/dist/cjs/consensus/index.js.map +1 -0
- package/dist/cjs/constructors.d.ts +13 -0
- package/dist/cjs/constructors.d.ts.map +1 -0
- package/dist/cjs/constructors.js +80 -0
- package/dist/cjs/constructors.js.map +1 -0
- package/dist/cjs/db/cache.d.ts +17 -0
- package/dist/cjs/db/cache.d.ts.map +1 -0
- package/dist/cjs/db/cache.js +38 -0
- package/dist/cjs/db/cache.js.map +1 -0
- package/dist/cjs/db/constants.d.ts +23 -0
- package/dist/cjs/db/constants.d.ts.map +1 -0
- package/dist/cjs/db/constants.js +54 -0
- package/dist/cjs/db/constants.js.map +1 -0
- package/dist/cjs/db/helpers.d.ts +9 -0
- package/dist/cjs/db/helpers.d.ts.map +1 -0
- package/dist/cjs/db/helpers.js +68 -0
- package/dist/cjs/db/helpers.js.map +1 -0
- package/dist/cjs/db/manager.d.ts +78 -0
- package/dist/cjs/db/manager.d.ts.map +1 -0
- package/dist/cjs/db/manager.js +203 -0
- package/dist/cjs/db/manager.js.map +1 -0
- package/dist/cjs/db/operation.d.ts +45 -0
- package/dist/cjs/db/operation.d.ts.map +1 -0
- package/dist/cjs/db/operation.js +110 -0
- package/dist/cjs/db/operation.js.map +1 -0
- package/dist/cjs/helpers.d.ts +19 -0
- package/dist/cjs/helpers.d.ts.map +1 -0
- package/dist/cjs/helpers.js +34 -0
- package/dist/cjs/helpers.js.map +1 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/types.d.ts +219 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/blockchain.d.ts +372 -0
- package/dist/esm/blockchain.d.ts.map +1 -0
- package/dist/esm/blockchain.js +1101 -0
- package/dist/esm/blockchain.js.map +1 -0
- package/dist/esm/consensus/casper.d.ts +16 -0
- package/dist/esm/consensus/casper.d.ts.map +1 -0
- package/dist/esm/consensus/casper.js +24 -0
- package/dist/esm/consensus/casper.js.map +1 -0
- package/dist/esm/consensus/clique.d.ts +164 -0
- package/dist/esm/consensus/clique.d.ts.map +1 -0
- package/dist/esm/consensus/clique.js +516 -0
- package/dist/esm/consensus/clique.js.map +1 -0
- package/dist/esm/consensus/ethash.d.ts +29 -0
- package/dist/esm/consensus/ethash.d.ts.map +1 -0
- package/dist/esm/consensus/ethash.js +44 -0
- package/dist/esm/consensus/ethash.js.map +1 -0
- package/dist/esm/consensus/index.d.ts +5 -0
- package/dist/esm/consensus/index.d.ts.map +1 -0
- package/dist/esm/consensus/index.js +5 -0
- package/dist/esm/consensus/index.js.map +1 -0
- package/dist/esm/constructors.d.ts +13 -0
- package/dist/esm/constructors.d.ts.map +1 -0
- package/dist/esm/constructors.js +76 -0
- package/dist/esm/constructors.js.map +1 -0
- package/dist/esm/db/cache.d.ts +17 -0
- package/dist/esm/db/cache.d.ts.map +1 -0
- package/dist/esm/db/cache.js +34 -0
- package/dist/esm/db/cache.js.map +1 -0
- package/dist/esm/db/constants.d.ts +23 -0
- package/dist/esm/db/constants.d.ts.map +1 -0
- package/dist/esm/db/constants.js +46 -0
- package/dist/esm/db/constants.js.map +1 -0
- package/dist/esm/db/helpers.d.ts +9 -0
- package/dist/esm/db/helpers.d.ts.map +1 -0
- package/dist/esm/db/helpers.js +61 -0
- package/dist/esm/db/helpers.js.map +1 -0
- package/dist/esm/db/manager.d.ts +78 -0
- package/dist/esm/db/manager.d.ts.map +1 -0
- package/dist/esm/db/manager.js +199 -0
- package/dist/esm/db/manager.js.map +1 -0
- package/dist/esm/db/operation.d.ts +45 -0
- package/dist/esm/db/operation.d.ts.map +1 -0
- package/dist/esm/db/operation.js +106 -0
- package/dist/esm/db/operation.js.map +1 -0
- package/dist/esm/helpers.d.ts +19 -0
- package/dist/esm/helpers.d.ts.map +1 -0
- package/dist/esm/helpers.js +30 -0
- package/dist/esm/helpers.js.map +1 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/types.d.ts +219 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
- package/package.json +75 -0
- package/src/blockchain.ts +1353 -0
- package/src/consensus/casper.ts +33 -0
- package/src/consensus/clique.ts +633 -0
- package/src/consensus/ethash.ts +69 -0
- package/src/consensus/index.ts +5 -0
- package/src/constructors.ts +119 -0
- package/src/db/cache.ts +39 -0
- package/src/db/constants.ts +73 -0
- package/src/db/helpers.ts +81 -0
- package/src/db/manager.ts +243 -0
- package/src/db/operation.ts +152 -0
- package/src/helpers.ts +37 -0
- package/src/index.ts +12 -0
- package/src/types.ts +256 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ConsensusAlgorithm } from '@feelyourprotocol/common'
|
|
2
|
+
import { BIGINT_0, EthereumJSErrorWithoutCode } from '@feelyourprotocol/util'
|
|
3
|
+
|
|
4
|
+
import type { BlockHeader } from '@feelyourprotocol/block'
|
|
5
|
+
import type { Consensus } from '../types.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This class encapsulates Casper-related consensus functionality when used with the Blockchain class.
|
|
9
|
+
*/
|
|
10
|
+
export class CasperConsensus implements Consensus {
|
|
11
|
+
algorithm: ConsensusAlgorithm
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.algorithm = ConsensusAlgorithm.Casper
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async genesisInit(): Promise<void> {}
|
|
18
|
+
|
|
19
|
+
public async setup(): Promise<void> {}
|
|
20
|
+
|
|
21
|
+
public async validateConsensus(): Promise<void> {}
|
|
22
|
+
|
|
23
|
+
public async validateDifficulty(header: BlockHeader): Promise<void> {
|
|
24
|
+
// TODO: This is not really part of consensus validation and it should be analyzed
|
|
25
|
+
// if it is possible to replace by a more generic hardfork check between block and
|
|
26
|
+
// blockchain along adding new blocks or headers
|
|
27
|
+
if (header.difficulty !== BIGINT_0) {
|
|
28
|
+
const msg = 'invalid difficulty. PoS blocks must have difficulty 0'
|
|
29
|
+
throw EthereumJSErrorWithoutCode(`${msg} ${header.errorStr()}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
public async newBlock(): Promise<void> {}
|
|
33
|
+
}
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cliqueEpochTransitionSigners,
|
|
3
|
+
cliqueIsEpochTransition,
|
|
4
|
+
cliqueSigner,
|
|
5
|
+
cliqueVerifySignature,
|
|
6
|
+
} from '@feelyourprotocol/block'
|
|
7
|
+
import { ConsensusAlgorithm } from '@feelyourprotocol/common'
|
|
8
|
+
import { RLP } from '@feelyourprotocol/rlp'
|
|
9
|
+
import {
|
|
10
|
+
Address,
|
|
11
|
+
BIGINT_0,
|
|
12
|
+
BIGINT_1,
|
|
13
|
+
BIGINT_2,
|
|
14
|
+
EthereumJSErrorWithoutCode,
|
|
15
|
+
type NestedUint8Array,
|
|
16
|
+
TypeOutput,
|
|
17
|
+
bigIntToBytes,
|
|
18
|
+
bytesToBigInt,
|
|
19
|
+
equalsBytes,
|
|
20
|
+
hexToBytes,
|
|
21
|
+
isDebugEnabled,
|
|
22
|
+
toType,
|
|
23
|
+
} from '@feelyourprotocol/util'
|
|
24
|
+
import debugDefault from 'debug'
|
|
25
|
+
|
|
26
|
+
import type { Block, BlockHeader } from '@feelyourprotocol/block'
|
|
27
|
+
import type { CliqueConfig } from '@feelyourprotocol/common'
|
|
28
|
+
import type { Blockchain } from '../index.ts'
|
|
29
|
+
import type { Consensus, ConsensusOptions } from '../types.ts'
|
|
30
|
+
|
|
31
|
+
const debug = debugDefault('blockchain:clique')
|
|
32
|
+
|
|
33
|
+
// Magic nonce number to vote on adding a new signer
|
|
34
|
+
export const CLIQUE_NONCE_AUTH = new Uint8Array(
|
|
35
|
+
hexToBytes('0xffffffffffffffff').buffer as ArrayBuffer,
|
|
36
|
+
)
|
|
37
|
+
// Magic nonce number to vote on removing a signer.
|
|
38
|
+
export const CLIQUE_NONCE_DROP = new Uint8Array(8)
|
|
39
|
+
|
|
40
|
+
const CLIQUE_SIGNERS_KEY = 'CliqueSigners'
|
|
41
|
+
const CLIQUE_VOTES_KEY = 'CliqueVotes'
|
|
42
|
+
const CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY = 'CliqueBlockSignersSnapshot'
|
|
43
|
+
|
|
44
|
+
// Block difficulty for in-turn signatures
|
|
45
|
+
export const CLIQUE_DIFF_INTURN = BIGINT_2
|
|
46
|
+
// Block difficulty for out-of-turn signatures
|
|
47
|
+
export const CLIQUE_DIFF_NOTURN = BIGINT_1
|
|
48
|
+
|
|
49
|
+
// Clique Signer State
|
|
50
|
+
type CliqueSignerState = [blockNumber: bigint, signers: Address[]]
|
|
51
|
+
type CliqueLatestSignerStates = CliqueSignerState[]
|
|
52
|
+
|
|
53
|
+
// Clique Vote
|
|
54
|
+
type CliqueVote = [
|
|
55
|
+
blockNumber: bigint,
|
|
56
|
+
vote: [signer: Address, beneficiary: Address, cliqueNonce: Uint8Array],
|
|
57
|
+
]
|
|
58
|
+
type CliqueLatestVotes = CliqueVote[]
|
|
59
|
+
|
|
60
|
+
// Clique Block Signer
|
|
61
|
+
type CliqueBlockSigner = [blockNumber: bigint, signer: Address]
|
|
62
|
+
type CliqueLatestBlockSigners = CliqueBlockSigner[]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This class encapsulates Clique-related consensus functionality when used with the Blockchain class.
|
|
66
|
+
* Note: reorgs which happen between epoch transitions, which change the internal voting state over the reorg
|
|
67
|
+
* will result in failure and is currently not supported.
|
|
68
|
+
* The hotfix for this could be: re-load the latest epoch block (this has the clique state in the extraData of the header)
|
|
69
|
+
* Now replay all blocks on top of it. This should validate the chain up to the new/reorged tip which previously threw.
|
|
70
|
+
*/
|
|
71
|
+
export class CliqueConsensus implements Consensus {
|
|
72
|
+
blockchain: Blockchain | undefined
|
|
73
|
+
algorithm: ConsensusAlgorithm
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Keep signer history data (signer states and votes)
|
|
77
|
+
* for all block numbers >= HEAD_BLOCK - CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
|
|
78
|
+
*
|
|
79
|
+
* This defines a limit for reorgs on PoA clique chains.
|
|
80
|
+
*/
|
|
81
|
+
private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 200
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List with the latest signer states checkpointed on blocks where
|
|
85
|
+
* a change (added new or removed a signer) occurred.
|
|
86
|
+
*
|
|
87
|
+
* Format:
|
|
88
|
+
* [ [BLOCK_NUMBER_1, [SIGNER1, SIGNER 2,]], [BLOCK_NUMBER2, [SIGNER1, SIGNER3]], ...]
|
|
89
|
+
*
|
|
90
|
+
* The top element from the array represents the list of current signers.
|
|
91
|
+
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
|
|
92
|
+
*
|
|
93
|
+
* Always keep at least one item on the stack.
|
|
94
|
+
*/
|
|
95
|
+
public _cliqueLatestSignerStates: CliqueLatestSignerStates = []
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List with the latest signer votes.
|
|
99
|
+
*
|
|
100
|
+
* Format:
|
|
101
|
+
* [ [BLOCK_NUMBER_1, [SIGNER, BENEFICIARY, AUTH]], [BLOCK_NUMBER_1, [SIGNER, BENEFICIARY, AUTH]] ]
|
|
102
|
+
* where AUTH = CLIQUE_NONCE_AUTH | CLIQUE_NONCE_DROP
|
|
103
|
+
*
|
|
104
|
+
* For votes all elements here must be taken into account with a
|
|
105
|
+
* block number >= LAST_EPOCH_BLOCK
|
|
106
|
+
* (nevertheless keep entries with blocks before EPOCH_BLOCK in case a reorg happens
|
|
107
|
+
* during an epoch change)
|
|
108
|
+
*
|
|
109
|
+
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
|
|
110
|
+
*/
|
|
111
|
+
public _cliqueLatestVotes: CliqueLatestVotes = []
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List of signers for the last consecutive {@link Blockchain.cliqueSignerLimit} blocks.
|
|
115
|
+
* Kept as a snapshot for quickly checking for "recently signed" error.
|
|
116
|
+
* Format: [ [BLOCK_NUMBER, SIGNER_ADDRESS], ...]
|
|
117
|
+
*
|
|
118
|
+
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
|
|
119
|
+
*/
|
|
120
|
+
public _cliqueLatestBlockSigners: CliqueLatestBlockSigners = []
|
|
121
|
+
|
|
122
|
+
DEBUG: boolean // Guard for debug logs
|
|
123
|
+
constructor() {
|
|
124
|
+
// Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables
|
|
125
|
+
this.DEBUG = isDebugEnabled('ethjs')
|
|
126
|
+
|
|
127
|
+
this.algorithm = ConsensusAlgorithm.Clique
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
*
|
|
132
|
+
* @param param dictionary containing a {@link Blockchain} object
|
|
133
|
+
*
|
|
134
|
+
* Note: this method must be called before consensus checks are used or type errors will occur
|
|
135
|
+
*/
|
|
136
|
+
async setup({ blockchain }: ConsensusOptions): Promise<void> {
|
|
137
|
+
this.blockchain = blockchain
|
|
138
|
+
this._cliqueLatestSignerStates = await this.getCliqueLatestSignerStates()
|
|
139
|
+
this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1))
|
|
140
|
+
this._cliqueLatestVotes = await this.getCliqueLatestVotes()
|
|
141
|
+
this._cliqueLatestBlockSigners = await this.getCliqueLatestBlockSigners()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async genesisInit(genesisBlock: Block): Promise<void> {
|
|
145
|
+
await this.cliqueSaveGenesisSigners(genesisBlock)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async validateConsensus(block: Block): Promise<void> {
|
|
149
|
+
if (!this.blockchain) {
|
|
150
|
+
throw EthereumJSErrorWithoutCode('blockchain not provided')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { header } = block
|
|
154
|
+
const valid = cliqueVerifySignature(header, this.cliqueActiveSigners(header.number))
|
|
155
|
+
if (!valid) {
|
|
156
|
+
throw EthereumJSErrorWithoutCode('invalid PoA block signature (clique)')
|
|
157
|
+
}
|
|
158
|
+
if (this.cliqueCheckRecentlySigned(header)) {
|
|
159
|
+
throw EthereumJSErrorWithoutCode('recently signed')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// validate checkpoint signers towards active signers on epoch transition blocks
|
|
163
|
+
if (cliqueIsEpochTransition(header)) {
|
|
164
|
+
// note: keep votes on epoch transition blocks in case of reorgs.
|
|
165
|
+
// only active (non-stale) votes will counted (if vote.blockNumber >= lastEpochBlockNumber
|
|
166
|
+
|
|
167
|
+
const checkpointSigners = cliqueEpochTransitionSigners(header)
|
|
168
|
+
const activeSigners = this.cliqueActiveSigners(header.number)
|
|
169
|
+
for (const [i, cSigner] of checkpointSigners.entries()) {
|
|
170
|
+
if (activeSigners[i]?.equals(cSigner) !== true) {
|
|
171
|
+
throw EthereumJSErrorWithoutCode(
|
|
172
|
+
`checkpoint signer not found in active signers list at index ${i}: ${cSigner}`,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async validateDifficulty(header: BlockHeader): Promise<void> {
|
|
180
|
+
if (!this.blockchain) {
|
|
181
|
+
throw EthereumJSErrorWithoutCode('blockchain not provided')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (header.difficulty !== CLIQUE_DIFF_INTURN && header.difficulty !== CLIQUE_DIFF_NOTURN) {
|
|
185
|
+
const msg = `difficulty for clique block must be INTURN (2) or NOTURN (1), received: ${header.difficulty}`
|
|
186
|
+
throw EthereumJSErrorWithoutCode(`${msg} ${header.errorStr()}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const signers = this.cliqueActiveSigners(header.number)
|
|
190
|
+
if (signers.length === 0) {
|
|
191
|
+
// abort if signers are unavailable
|
|
192
|
+
const msg = 'no signers available'
|
|
193
|
+
throw EthereumJSErrorWithoutCode(`${msg} ${header.errorStr()}`)
|
|
194
|
+
}
|
|
195
|
+
const signerIndex = signers.findIndex((address: Address) =>
|
|
196
|
+
address.equals(cliqueSigner(header)),
|
|
197
|
+
)
|
|
198
|
+
const inTurn = header.number % BigInt(signers.length) === BigInt(signerIndex)
|
|
199
|
+
if (
|
|
200
|
+
(inTurn && header.difficulty === CLIQUE_DIFF_INTURN) ||
|
|
201
|
+
(!inTurn && header.difficulty === CLIQUE_DIFF_NOTURN)
|
|
202
|
+
) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
throw EthereumJSErrorWithoutCode(`'invalid clique difficulty ${header.errorStr()}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async newBlock(block: Block, commonAncestor: BlockHeader | undefined): Promise<void> {
|
|
209
|
+
// Clique: update signer votes and state
|
|
210
|
+
const { header } = block
|
|
211
|
+
const commonAncestorNumber = commonAncestor?.number
|
|
212
|
+
if (commonAncestorNumber !== undefined) {
|
|
213
|
+
await this._cliqueDeleteSnapshots(commonAncestorNumber + BIGINT_1)
|
|
214
|
+
for (let number = commonAncestorNumber + BigInt(1); number <= header.number; number++) {
|
|
215
|
+
const canonicalHeader = await this.blockchain!.getCanonicalHeader(number)
|
|
216
|
+
await this._cliqueBuildSnapshots(canonicalHeader)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Save genesis signers to db
|
|
223
|
+
* @param genesisBlock genesis block
|
|
224
|
+
* @hidden
|
|
225
|
+
*/
|
|
226
|
+
private async cliqueSaveGenesisSigners(genesisBlock: Block) {
|
|
227
|
+
const genesisSignerState: CliqueSignerState = [
|
|
228
|
+
BIGINT_0,
|
|
229
|
+
cliqueEpochTransitionSigners(genesisBlock.header),
|
|
230
|
+
]
|
|
231
|
+
await this.cliqueUpdateSignerStates(genesisSignerState)
|
|
232
|
+
this.DEBUG && debug(`[Block 0] Genesis block -> update signer states`)
|
|
233
|
+
await this.cliqueUpdateVotes()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Save signer state to db
|
|
238
|
+
* @param signerState
|
|
239
|
+
* @hidden
|
|
240
|
+
*/
|
|
241
|
+
private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) {
|
|
242
|
+
if (signerState) {
|
|
243
|
+
const blockNumber = signerState[0]
|
|
244
|
+
const known = this._cliqueLatestSignerStates.find((value) => {
|
|
245
|
+
if (value[0] === blockNumber) {
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
if (known !== undefined) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
this._cliqueLatestSignerStates.push(signerState)
|
|
253
|
+
this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// trim to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
|
|
257
|
+
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
|
|
258
|
+
const blockSigners = this._cliqueLatestBlockSigners
|
|
259
|
+
const lastBlockNumber = blockSigners[blockSigners.length - 1]?.[0]
|
|
260
|
+
if (lastBlockNumber) {
|
|
261
|
+
const blockLimit = lastBlockNumber - BigInt(limit)
|
|
262
|
+
const states = this._cliqueLatestSignerStates
|
|
263
|
+
const lastItem = states[states.length - 1]
|
|
264
|
+
this._cliqueLatestSignerStates = states.filter((state) => state[0] >= blockLimit)
|
|
265
|
+
if (this._cliqueLatestSignerStates.length === 0) {
|
|
266
|
+
// always keep at least one item on the stack
|
|
267
|
+
this._cliqueLatestSignerStates.push(lastItem)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// save to db
|
|
272
|
+
const formatted = this._cliqueLatestSignerStates.map((state) => [
|
|
273
|
+
bigIntToBytes(state[0]),
|
|
274
|
+
state[1].map((a) => a.toBytes()),
|
|
275
|
+
])
|
|
276
|
+
await this.blockchain!.db.put(CLIQUE_SIGNERS_KEY, RLP.encode(formatted))
|
|
277
|
+
// Output active signers for debugging purposes
|
|
278
|
+
if (signerState !== undefined) {
|
|
279
|
+
let i = 0
|
|
280
|
+
try {
|
|
281
|
+
for (const signer of this.cliqueActiveSigners(signerState[0])) {
|
|
282
|
+
this.DEBUG && debug(`Clique signer [${i}]: ${signer} (block: ${signerState[0]})`)
|
|
283
|
+
i++
|
|
284
|
+
}
|
|
285
|
+
// eslint-disable-next-line no-empty
|
|
286
|
+
} catch {}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Update clique votes and save to db
|
|
292
|
+
* @param header BlockHeader
|
|
293
|
+
* @hidden
|
|
294
|
+
*/
|
|
295
|
+
private async cliqueUpdateVotes(header?: BlockHeader) {
|
|
296
|
+
// Block contains a vote on a new signer
|
|
297
|
+
if (header && !header.coinbase.isZero()) {
|
|
298
|
+
const signer = cliqueSigner(header)
|
|
299
|
+
const beneficiary = header.coinbase
|
|
300
|
+
const nonce = header.nonce
|
|
301
|
+
const latestVote: CliqueVote = [header.number, [signer, beneficiary, nonce]]
|
|
302
|
+
|
|
303
|
+
// Do two rounds here, one to execute on a potential previously reached consensus
|
|
304
|
+
// on the newly touched beneficiary, one with the added new vote
|
|
305
|
+
for (let round = 1; round <= 2; round++) {
|
|
306
|
+
// See if there is a new majority consensus to update the signer list
|
|
307
|
+
const lastEpochBlockNumber =
|
|
308
|
+
header.number -
|
|
309
|
+
(header.number %
|
|
310
|
+
BigInt((this.blockchain!.common.consensusConfig() as CliqueConfig).epoch))
|
|
311
|
+
const limit = this.cliqueSignerLimit(header.number)
|
|
312
|
+
let activeSigners = [...this.cliqueActiveSigners(header.number)]
|
|
313
|
+
let consensus = false
|
|
314
|
+
|
|
315
|
+
// AUTH vote analysis
|
|
316
|
+
let votes = this._cliqueLatestVotes.filter((vote) => {
|
|
317
|
+
return (
|
|
318
|
+
vote[0] >= BigInt(lastEpochBlockNumber) &&
|
|
319
|
+
!vote[1][0].equals(signer) &&
|
|
320
|
+
vote[1][1].equals(beneficiary) &&
|
|
321
|
+
equalsBytes(vote[1][2], CLIQUE_NONCE_AUTH)
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
const beneficiaryVotesAUTH: Address[] = []
|
|
325
|
+
for (const vote of votes) {
|
|
326
|
+
const num = beneficiaryVotesAUTH.filter((voteCMP) => {
|
|
327
|
+
return voteCMP.equals(vote[1][0])
|
|
328
|
+
}).length
|
|
329
|
+
if (num === 0) {
|
|
330
|
+
beneficiaryVotesAUTH.push(vote[1][0])
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
let numBeneficiaryVotesAUTH = beneficiaryVotesAUTH.length
|
|
334
|
+
if (round === 2 && equalsBytes(nonce, CLIQUE_NONCE_AUTH)) {
|
|
335
|
+
numBeneficiaryVotesAUTH += 1
|
|
336
|
+
}
|
|
337
|
+
// Majority consensus
|
|
338
|
+
if (numBeneficiaryVotesAUTH >= limit) {
|
|
339
|
+
consensus = true
|
|
340
|
+
// Authorize new signer
|
|
341
|
+
activeSigners.push(beneficiary)
|
|
342
|
+
activeSigners.sort((a, b) => {
|
|
343
|
+
// Sort by array size
|
|
344
|
+
const result =
|
|
345
|
+
toType(a.toString(), TypeOutput.BigInt) < toType(b.toString(), TypeOutput.BigInt)
|
|
346
|
+
if (result) {
|
|
347
|
+
return -1
|
|
348
|
+
} else {
|
|
349
|
+
return 1
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
// Discard votes for added signer
|
|
353
|
+
this._cliqueLatestVotes = this._cliqueLatestVotes.filter(
|
|
354
|
+
(vote) => !vote[1][1].equals(beneficiary),
|
|
355
|
+
)
|
|
356
|
+
this.DEBUG &&
|
|
357
|
+
debug(`[Block ${header.number}] Clique majority consensus (AUTH ${beneficiary})`)
|
|
358
|
+
}
|
|
359
|
+
// DROP vote
|
|
360
|
+
votes = this._cliqueLatestVotes.filter((vote) => {
|
|
361
|
+
return (
|
|
362
|
+
vote[0] >= BigInt(lastEpochBlockNumber) &&
|
|
363
|
+
!vote[1][0].equals(signer) &&
|
|
364
|
+
vote[1][1].equals(beneficiary) &&
|
|
365
|
+
equalsBytes(vote[1][2], CLIQUE_NONCE_DROP)
|
|
366
|
+
)
|
|
367
|
+
})
|
|
368
|
+
const beneficiaryVotesDROP: Address[] = []
|
|
369
|
+
for (const vote of votes) {
|
|
370
|
+
const num = beneficiaryVotesDROP.filter((voteCMP) => {
|
|
371
|
+
return voteCMP.equals(vote[1][0])
|
|
372
|
+
}).length
|
|
373
|
+
if (num === 0) {
|
|
374
|
+
beneficiaryVotesDROP.push(vote[1][0])
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
let numBeneficiaryVotesDROP = beneficiaryVotesDROP.length
|
|
378
|
+
|
|
379
|
+
if (round === 2 && equalsBytes(nonce, CLIQUE_NONCE_DROP)) {
|
|
380
|
+
numBeneficiaryVotesDROP += 1
|
|
381
|
+
}
|
|
382
|
+
// Majority consensus
|
|
383
|
+
if (numBeneficiaryVotesDROP >= limit) {
|
|
384
|
+
consensus = true
|
|
385
|
+
// Drop signer
|
|
386
|
+
activeSigners = activeSigners.filter((signer) => !signer.equals(beneficiary))
|
|
387
|
+
this._cliqueLatestVotes = this._cliqueLatestVotes.filter(
|
|
388
|
+
// Discard votes from removed signer and for removed signer
|
|
389
|
+
(vote) => !vote[1][0].equals(beneficiary) && !vote[1][1].equals(beneficiary),
|
|
390
|
+
)
|
|
391
|
+
this.DEBUG &&
|
|
392
|
+
debug(`[Block ${header.number}] Clique majority consensus (DROP ${beneficiary})`)
|
|
393
|
+
}
|
|
394
|
+
if (round === 1) {
|
|
395
|
+
// Always add the latest vote to the history no matter if already voted
|
|
396
|
+
// the same vote or not
|
|
397
|
+
this._cliqueLatestVotes.push(latestVote)
|
|
398
|
+
this.DEBUG &&
|
|
399
|
+
debug(
|
|
400
|
+
`[Block ${header.number}] New clique vote: ${signer} -> ${beneficiary} ${
|
|
401
|
+
equalsBytes(nonce, CLIQUE_NONCE_AUTH) ? 'AUTH' : 'DROP'
|
|
402
|
+
}`,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
if (consensus) {
|
|
406
|
+
if (round === 1) {
|
|
407
|
+
this.DEBUG &&
|
|
408
|
+
debug(
|
|
409
|
+
`[Block ${header.number}] Clique majority consensus on existing votes -> update signer states`,
|
|
410
|
+
)
|
|
411
|
+
} else {
|
|
412
|
+
this.DEBUG &&
|
|
413
|
+
debug(
|
|
414
|
+
`[Block ${header.number}] Clique majority consensus on new vote -> update signer states`,
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
const newSignerState: CliqueSignerState = [header.number, activeSigners]
|
|
418
|
+
await this.cliqueUpdateSignerStates(newSignerState)
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// trim to lastEpochBlockNumber - CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
|
|
425
|
+
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
|
|
426
|
+
const blockSigners = this._cliqueLatestBlockSigners
|
|
427
|
+
const lastBlockNumber = blockSigners[blockSigners.length - 1]?.[0]
|
|
428
|
+
if (lastBlockNumber) {
|
|
429
|
+
const lastEpochBlockNumber =
|
|
430
|
+
lastBlockNumber -
|
|
431
|
+
(lastBlockNumber %
|
|
432
|
+
BigInt((this.blockchain!.common.consensusConfig() as CliqueConfig).epoch))
|
|
433
|
+
const blockLimit = lastEpochBlockNumber - BigInt(limit)
|
|
434
|
+
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((state) => state[0] >= blockLimit)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// save votes to db
|
|
438
|
+
const formatted = this._cliqueLatestVotes.map((v) => [
|
|
439
|
+
bigIntToBytes(v[0]),
|
|
440
|
+
[v[1][0].toBytes(), v[1][1].toBytes(), v[1][2]],
|
|
441
|
+
])
|
|
442
|
+
await this.blockchain!.db.put(CLIQUE_VOTES_KEY, RLP.encode(formatted))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Returns a list with the current block signers
|
|
447
|
+
*/
|
|
448
|
+
cliqueActiveSigners(blockNum: bigint): Address[] {
|
|
449
|
+
const signers = this._cliqueLatestSignerStates
|
|
450
|
+
if (signers.length === 0) {
|
|
451
|
+
return []
|
|
452
|
+
}
|
|
453
|
+
for (let i = signers.length - 1; i >= 0; i--) {
|
|
454
|
+
if (signers[i][0] < blockNum) {
|
|
455
|
+
return signers[i][1]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
throw EthereumJSErrorWithoutCode(`Could not load signers for block ${blockNum}`)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Number of consecutive blocks out of which a signer may only sign one.
|
|
463
|
+
* Defined as `Math.floor(SIGNER_COUNT / 2) + 1` to enforce majority consensus.
|
|
464
|
+
* signer count -> signer limit:
|
|
465
|
+
* 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3, ...
|
|
466
|
+
* @hidden
|
|
467
|
+
*/
|
|
468
|
+
private cliqueSignerLimit(blockNum: bigint) {
|
|
469
|
+
return Math.floor(this.cliqueActiveSigners(blockNum).length / 2) + 1
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Checks if signer was recently signed.
|
|
474
|
+
* Returns true if signed too recently: more than once per {@link CliqueConsensus.cliqueSignerLimit} consecutive blocks.
|
|
475
|
+
* @param header BlockHeader
|
|
476
|
+
* @hidden
|
|
477
|
+
*/
|
|
478
|
+
private cliqueCheckRecentlySigned(header: BlockHeader): boolean {
|
|
479
|
+
if (header.isGenesis() || header.number === BigInt(1)) {
|
|
480
|
+
// skip genesis, first block
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
const limit = this.cliqueSignerLimit(header.number)
|
|
484
|
+
// construct recent block signers list with this block
|
|
485
|
+
let signers = this._cliqueLatestBlockSigners
|
|
486
|
+
signers = signers.slice(signers.length < limit ? 0 : 1)
|
|
487
|
+
if (signers.length > 0 && signers[signers.length - 1][0] !== header.number - BigInt(1)) {
|
|
488
|
+
// if the last signed block is not one minus the head we are trying to compare
|
|
489
|
+
// we do not have a complete picture of the state to verify if too recently signed
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
signers.push([header.number, cliqueSigner(header)])
|
|
493
|
+
const seen = signers.filter((s) => s[1].equals(cliqueSigner(header))).length
|
|
494
|
+
return seen > 1
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Remove clique snapshots with blockNumber higher than input.
|
|
499
|
+
* @param blockNumber - the block number from which we start deleting
|
|
500
|
+
* @hidden
|
|
501
|
+
*/
|
|
502
|
+
private async _cliqueDeleteSnapshots(blockNumber: bigint) {
|
|
503
|
+
// remove blockNumber from clique snapshots
|
|
504
|
+
// (latest signer states, latest votes, latest block signers)
|
|
505
|
+
this._cliqueLatestSignerStates = this._cliqueLatestSignerStates.filter(
|
|
506
|
+
(s) => s[0] <= blockNumber,
|
|
507
|
+
)
|
|
508
|
+
await this.cliqueUpdateSignerStates()
|
|
509
|
+
|
|
510
|
+
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((v) => v[0] <= blockNumber)
|
|
511
|
+
await this.cliqueUpdateVotes()
|
|
512
|
+
|
|
513
|
+
this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.filter(
|
|
514
|
+
(s) => s[0] <= blockNumber,
|
|
515
|
+
)
|
|
516
|
+
await this.cliqueUpdateLatestBlockSigners()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Update snapshot of latest clique block signers.
|
|
521
|
+
* Used for checking for 'recently signed' error.
|
|
522
|
+
* Length trimmed to {@link Blockchain.cliqueSignerLimit}.
|
|
523
|
+
* @param header BlockHeader
|
|
524
|
+
* @hidden
|
|
525
|
+
*/
|
|
526
|
+
private async cliqueUpdateLatestBlockSigners(header?: BlockHeader) {
|
|
527
|
+
if (header) {
|
|
528
|
+
if (header.isGenesis()) {
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
// add this block's signer
|
|
532
|
+
const signer: CliqueBlockSigner = [header.number, cliqueSigner(header)]
|
|
533
|
+
this._cliqueLatestBlockSigners.push(signer)
|
|
534
|
+
|
|
535
|
+
// trim length to `this.cliqueSignerLimit()`
|
|
536
|
+
const length = this._cliqueLatestBlockSigners.length
|
|
537
|
+
const limit = this.cliqueSignerLimit(header.number)
|
|
538
|
+
if (length > limit) {
|
|
539
|
+
this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice(
|
|
540
|
+
length - limit,
|
|
541
|
+
length,
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// save to db
|
|
547
|
+
const formatted = this._cliqueLatestBlockSigners.map((b) => [
|
|
548
|
+
bigIntToBytes(b[0]),
|
|
549
|
+
b[1].toBytes(),
|
|
550
|
+
])
|
|
551
|
+
await this.blockchain!.db.put(CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY, RLP.encode(formatted))
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Fetches clique signers.
|
|
556
|
+
* @hidden
|
|
557
|
+
*/
|
|
558
|
+
private async getCliqueLatestSignerStates(): Promise<CliqueLatestSignerStates> {
|
|
559
|
+
const signerStates = await this.blockchain!.db.get(CLIQUE_SIGNERS_KEY)
|
|
560
|
+
if (signerStates === undefined) return []
|
|
561
|
+
const states = RLP.decode(signerStates as Uint8Array) as NestedUint8Array
|
|
562
|
+
return states.map((state) => {
|
|
563
|
+
const blockNum = bytesToBigInt(state[0] as Uint8Array)
|
|
564
|
+
const addresses: Address[] = (state[1] as Uint8Array[]).map((bytes: Uint8Array): Address => {
|
|
565
|
+
const address = new Address(bytes)
|
|
566
|
+
return address
|
|
567
|
+
})
|
|
568
|
+
return [blockNum, addresses]
|
|
569
|
+
}) as CliqueLatestSignerStates
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Fetches clique votes.
|
|
574
|
+
* @hidden
|
|
575
|
+
*/
|
|
576
|
+
private async getCliqueLatestVotes(): Promise<CliqueLatestVotes> {
|
|
577
|
+
const signerVotes = await this.blockchain!.db.get(CLIQUE_VOTES_KEY)
|
|
578
|
+
if (signerVotes === undefined) return []
|
|
579
|
+
const votes = RLP.decode(signerVotes as Uint8Array) as [
|
|
580
|
+
Uint8Array,
|
|
581
|
+
[Uint8Array, Uint8Array, Uint8Array],
|
|
582
|
+
]
|
|
583
|
+
return votes.map((vote) => {
|
|
584
|
+
const blockNum = bytesToBigInt(vote[0] as Uint8Array)
|
|
585
|
+
const signer = new Address((vote[1] as any)[0])
|
|
586
|
+
const beneficiary = new Address((vote[1] as any)[1])
|
|
587
|
+
const nonce = (vote[1] as any)[2]
|
|
588
|
+
return [blockNum, [signer, beneficiary, nonce]]
|
|
589
|
+
}) as CliqueLatestVotes
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Fetches snapshot of clique signers.
|
|
594
|
+
* @hidden
|
|
595
|
+
*/
|
|
596
|
+
private async getCliqueLatestBlockSigners(): Promise<CliqueLatestBlockSigners> {
|
|
597
|
+
const blockSigners = await this.blockchain!.db.get(CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY)
|
|
598
|
+
if (blockSigners === undefined) return []
|
|
599
|
+
const signers = RLP.decode(blockSigners as Uint8Array) as [Uint8Array, Uint8Array][]
|
|
600
|
+
return signers.map((s) => {
|
|
601
|
+
const blockNum = bytesToBigInt(s[0] as Uint8Array)
|
|
602
|
+
const signer = new Address(s[1])
|
|
603
|
+
return [blockNum, signer]
|
|
604
|
+
}) as CliqueLatestBlockSigners
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Build clique snapshots.
|
|
609
|
+
* @param header - the new block header
|
|
610
|
+
* @hidden
|
|
611
|
+
*/
|
|
612
|
+
private async _cliqueBuildSnapshots(header: BlockHeader) {
|
|
613
|
+
if (!cliqueIsEpochTransition(header)) {
|
|
614
|
+
await this.cliqueUpdateVotes(header)
|
|
615
|
+
}
|
|
616
|
+
await this.cliqueUpdateLatestBlockSigners(header)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Helper to determine if a signer is in or out of turn for the next block.
|
|
621
|
+
* @param signer The signer address
|
|
622
|
+
*/
|
|
623
|
+
async cliqueSignerInTurn(signer: Address, blockNum: bigint): Promise<boolean> {
|
|
624
|
+
const signers = this.cliqueActiveSigners(blockNum)
|
|
625
|
+
const signerIndex = signers.findIndex((address) => address.equals(signer))
|
|
626
|
+
if (signerIndex === -1) {
|
|
627
|
+
throw EthereumJSErrorWithoutCode('Signer not found')
|
|
628
|
+
}
|
|
629
|
+
const { number } = await this.blockchain!.getCanonicalHeadHeader()
|
|
630
|
+
|
|
631
|
+
return (number + BigInt(1)) % BigInt(signers.length) === BigInt(signerIndex)
|
|
632
|
+
}
|
|
633
|
+
}
|