Skip to content

Commit 7fb9ad8

Browse files
g11techacolytec3
andauthored
common: Refactor and update common.getHardforkByBlockNumber to address post merge hfs (#2313)
* common: Refactor and update the hardfork cal to address post merge hfs * make strick check optional for post merge hardforks * add tests for post merge hardforks using sepolia * simplify getHardforkByBlockNumber * remove duplicate hf * clarify comment * add post merge hf * further simplify the fn and add test to enhance coverage * update comments * remove hash to add it as a separate pr to not pollute this one * fix typo Co-authored-by: acolytec3 <[email protected]> * comment improvement * undo mainnet json changes * simplify dup ttd check * fix spec * fix typo Co-authored-by: acolytec3 <[email protected]> * fix typo Co-authored-by: acolytec3 <[email protected]> * improve clarity * simpilfy futher based on acolytec3s logic * fix some more validations * remove extra if * improve wording Co-authored-by: acolytec3 <[email protected]> * move spec, add test for merge hf with block num * relocate test * relocate test * lint * add merge block numbers * add merge forkhash * handle no hardfork found case * Fix nits Co-authored-by: acolytec3 <[email protected]>
1 parent 88bd15d commit 7fb9ad8

File tree

7 files changed

+208
-48
lines changed

7 files changed

+208
-48
lines changed

packages/client/test/rpc/util/CLConnectionManager.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ tape('[CLConnectionManager]', (t) => {
5353
manager = new CLConnectionManager({ config })
5454
st.ok(manager.running, 'starts on instantiation if hardfork is MergeForkBlock')
5555
manager.stop()
56+
const prevMergeForkBlock = (genesisJSON.config as any).mergeForkBlock
5657
;(genesisJSON.config as any).mergeForkBlock = 10
5758
common = new Common({
5859
chain: params.name,
@@ -69,6 +70,8 @@ tape('[CLConnectionManager]', (t) => {
6970
})
7071
config.events.emit(Event.CHAIN_UPDATED)
7172
config.events.emit(Event.CLIENT_SHUTDOWN)
73+
// reset prevMergeForkBlock as it seems to be polluting other tests
74+
;(genesisJSON.config as any).mergeForkBlock = prevMergeForkBlock
7275
})
7376

7477
t.test('Status updates', async (st) => {

packages/common/src/chains/goerli.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"forkHash": "0xb8c6299d"
7373
},
7474
{
75-
"//_comment": "The forkHash will remain same as mergeForkIdTransition is post merge",
75+
"//_comment": "The forkHash will remain same as mergeForkIdTransition is post merge, terminal block: https://goerli.etherscan.io/block/7382818",
7676
"name": "merge",
7777
"ttd": "10790000",
7878
"block": null,

packages/common/src/chains/mainnet.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,14 @@
8888
"forkHash": "0xf0afd0e3"
8989
},
9090
{
91-
"name": "mergeForkIdTransition",
91+
"//_comment": "The forkHash will remain same as mergeForkIdTransition is post merge, terminal block: https://etherscan.io/block/15537393",
92+
"name": "merge",
93+
"ttd": "58750000000000000000000",
9294
"block": null,
93-
"forkHash": null
95+
"forkHash": "0xf0afd0e3"
9496
},
9597
{
96-
"name": "merge",
97-
"ttd": "58750000000000000000000",
98+
"name": "mergeForkIdTransition",
9899
"block": null,
99100
"forkHash": null
100101
},

packages/common/src/chains/sepolia.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"forkHash": "0xfe3366e7"
7575
},
7676
{
77-
"//_comment": "The forkHash will remain same as mergeForkIdTransition is post merge",
77+
"//_comment": "The forkHash will remain same as mergeForkIdTransition is post merge, terminal block: https://sepolia.etherscan.io/block/1450408",
7878
"name": "merge",
7979
"ttd": "17000000000000000",
8080
"block": null,

packages/common/src/common.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -285,57 +285,63 @@ export class Common extends EventEmitter {
285285
* will be thrown).
286286
*
287287
* @param blockNumber
288-
* @param td
288+
* @param td : total difficulty of the parent block (for block hf) OR of the the chain latest (for chain hf)
289289
* @returns The name of the HF
290290
*/
291291
getHardforkByBlockNumber(blockNumber: BigIntLike, td?: BigIntLike): string {
292292
blockNumber = toType(blockNumber, TypeOutput.BigInt)
293293
td = toType(td, TypeOutput.BigInt)
294294

295-
let hardfork = Hardfork.Chainstart
296-
let minTdHF
297-
let maxTdHF
298-
let previousHF
299-
for (const hf of this.hardforks()) {
300-
// Skip comparison for not applied HFs
301-
if (hf.block === null) {
302-
if (td !== undefined && td !== null && hf.ttd !== undefined && hf.ttd !== null) {
303-
if (td >= BigInt(hf.ttd)) {
304-
return hf.name
305-
}
306-
}
307-
continue
308-
}
309-
if (blockNumber >= BigInt(hf.block)) {
310-
hardfork = hf.name as Hardfork
311-
}
312-
if (td && (typeof hf.ttd === 'string' || typeof hf.ttd === 'bigint')) {
313-
if (td >= BigInt(hf.ttd)) {
314-
minTdHF = hf.name
315-
} else {
316-
maxTdHF = previousHF
317-
}
318-
}
319-
previousHF = hf.name
320-
}
321-
if (td) {
322-
let msgAdd = `block number: ${blockNumber} (-> ${hardfork}), `
323-
if (minTdHF !== undefined) {
324-
if (!this.hardforkGteHardfork(hardfork, minTdHF)) {
325-
const msg = 'HF determined by block number is lower than the minimum total difficulty HF'
326-
msgAdd += `total difficulty: ${td} (-> ${minTdHF})`
327-
throw new Error(`${msg}: ${msgAdd}`)
328-
}
295+
// Filter out hardforks with no block number and no ttd (i.e. unapplied hardforks)
296+
const hfs = this.hardforks().filter(
297+
(hf) => hf.block !== null || (hf.ttd !== null && hf.ttd !== undefined)
298+
)
299+
const mergeIndex = hfs.findIndex((hf) => hf.ttd !== null && hf.ttd !== undefined)
300+
const doubleTTDHF = hfs
301+
.slice(mergeIndex + 1)
302+
.findIndex((hf) => hf.ttd !== null && hf.ttd !== undefined)
303+
if (doubleTTDHF >= 0) {
304+
throw Error(`More than one merge hardforks found with ttd specified`)
305+
}
306+
307+
// Find the first hardfork that has a block number greater than `blockNumber` (skips the merge hardfork since
308+
// it cannot have a block number specified).
309+
let hfIndex = hfs.findIndex((hf) => hf.block !== null && hf.block > blockNumber)
310+
311+
// Move hfIndex one back to arrive at candidate hardfork
312+
if (hfIndex === -1) {
313+
// all hardforks apply, set hfIndex to the last one as thats the candidate
314+
hfIndex = hfs.length - 1
315+
} else if (hfIndex === 0) {
316+
// cannot have a case where a block number is before all applied hardforks
317+
// since the chain has to start with a hardfork
318+
throw Error('Must have at least one hardfork at block 0')
319+
} else {
320+
// The previous hardfork is the candidate here
321+
hfIndex = hfIndex - 1
322+
}
323+
324+
let hardfork
325+
if (hfs[hfIndex].block === null) {
326+
// We're on the merge hardfork. Let's check the TTD
327+
if (td === undefined || td === null || BigInt(hfs[hfIndex].ttd!) > td) {
328+
// Merge ttd greater than current td so we're on hardfork before merge
329+
hardfork = hfs[hfIndex - 1]
330+
} else {
331+
// Merge ttd equal or less than current td so we're on merge hardfork
332+
hardfork = hfs[hfIndex]
329333
}
330-
if (maxTdHF !== undefined) {
331-
if (!this.hardforkGteHardfork(maxTdHF, hardfork)) {
332-
const msg = 'Maximum HF determined by total difficulty is lower than the block number HF'
333-
msgAdd += `total difficulty: ${td} (-> ${maxTdHF})`
334-
throw new Error(`${msg}: ${msgAdd}`)
334+
} else {
335+
if (mergeIndex >= 0 && td !== undefined && td !== null) {
336+
if (hfIndex >= mergeIndex && BigInt(hfs[mergeIndex].ttd!) > td) {
337+
throw Error('Maximum HF determined by total difficulty is lower than the block number HF')
338+
} else if (hfIndex < mergeIndex && BigInt(hfs[mergeIndex].ttd!) <= td) {
339+
throw Error('HF determined by block number is lower than the minimum total difficulty HF')
335340
}
336341
}
342+
hardfork = hfs[hfIndex]
337343
}
338-
return hardfork
344+
return hardfork.name
339345
}
340346

341347
/**

packages/common/tests/hardforks.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ tape('[Common]: Hardfork logic', function (t: tape.Test) {
6363
st.end()
6464
})
6565

66+
t.test('should throw if no hardfork qualifies', function (st) {
67+
const hardforks = [
68+
{
69+
name: 'homestead',
70+
block: 3,
71+
},
72+
{
73+
name: 'tangerineWhistle',
74+
block: 3,
75+
},
76+
{
77+
name: 'spuriousDragon',
78+
block: 3,
79+
},
80+
]
81+
const c = Common.custom({ hardforks }, { baseChain: Chain.Sepolia })
82+
83+
try {
84+
c.getHardforkByBlockNumber(0)
85+
t.fail('should have thrown since no hardfork should qualify')
86+
} catch (e) {
87+
t.pass('throw since no hardfork qualifies')
88+
}
89+
90+
const msg = 'should return correct value'
91+
st.equal(c.setHardforkByBlockNumber(3), Hardfork.SpuriousDragon, msg)
92+
93+
t.end()
94+
})
95+
6696
t.test('setHardfork(): hardforkChanged event', function (st) {
6797
const c = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Istanbul })
6898
c.on('hardforkChanged', (hardfork: string) => {

packages/common/tests/mergePOS.spec.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as tape from 'tape'
22

3-
import { Common, Hardfork } from '../src'
3+
import { Chain, Common, Hardfork } from '../src'
44

55
import * as testnetMerge from './data/merge/testnetMerge.json'
66
import * as testnetPOS from './data/merge/testnetPOS.json'
@@ -142,13 +142,15 @@ tape('[Common]: Merge/POS specific logic', function (t: tape.Test) {
142142

143143
try {
144144
c.setHardforkByBlockNumber(16, 4999)
145+
st.fail(`should have thrown td < ttd validation error`)
145146
} catch (e: any) {
146147
msg = 'block number > last HF block number set, TD set and smaller (should throw)'
147148
const eMsg = 'Maximum HF determined by total difficulty is lower than the block number HF'
148149
st.ok(e.message.includes(eMsg), msg)
149150
}
150151
try {
151152
c.setHardforkByBlockNumber(14, 5000)
153+
st.fail(`should have thrown td > ttd validation error`)
152154
} catch (e: any) {
153155
msg = 'block number < last HF block number set, TD set and higher (should throw)'
154156
const eMsg = 'HF determined by block number is lower than the minimum total difficulty HF'
@@ -187,4 +189,122 @@ tape('[Common]: Merge/POS specific logic', function (t: tape.Test) {
187189
st.equal(c.getHardforkByBlockNumber(5, 0), 'shanghai', msg)
188190
st.end()
189191
})
192+
193+
t.test('should get the correct merge hardfork at genesis', async (st) => {
194+
const json = require(`../../client/test/testdata/geth-genesis/post-merge.json`)
195+
const c = Common.fromGethGenesis(json, { chain: 'post-merge' })
196+
const msg = 'should get HF correctly'
197+
st.equal(c.getHardforkByBlockNumber(0), Hardfork.London, msg)
198+
st.equal(c.getHardforkByBlockNumber(0, BigInt(0)), Hardfork.Merge, msg)
199+
})
200+
201+
t.test('test post merge hardforks using Sepolia with block null', function (st: tape.Test) {
202+
const c = new Common({ chain: Chain.Sepolia })
203+
let msg = 'should get HF correctly'
204+
205+
st.equal(c.getHardforkByBlockNumber(0), Hardfork.London, msg)
206+
// Make it null manually as config could be updated later
207+
const mergeHf = c.hardforks().filter((hf) => hf.ttd !== undefined && hf.ttd !== null)[0]
208+
const prevMergeBlockVal = mergeHf.block
209+
mergeHf.block = null
210+
211+
// should get Hardfork.London even though happened with 1450408 as terminal as config doesn't have that info
212+
st.equal(c.getHardforkByBlockNumber(1450409), Hardfork.London, msg)
213+
// however with correct td in input it should select merge
214+
st.equal(c.getHardforkByBlockNumber(1450409, BigInt('17000000000000000')), Hardfork.Merge, msg)
215+
// should select MergeForkIdTransition even without td specified as the block is set for this hardfork
216+
st.equal(c.getHardforkByBlockNumber(1735371), Hardfork.MergeForkIdTransition, msg)
217+
// also with td specified
218+
st.equal(
219+
c.getHardforkByBlockNumber(1735371, BigInt('17000000000000000')),
220+
Hardfork.MergeForkIdTransition,
221+
msg
222+
)
223+
try {
224+
st.equal(
225+
c.getHardforkByBlockNumber(1735371, BigInt('15000000000000000')),
226+
Hardfork.MergeForkIdTransition,
227+
msg
228+
)
229+
st.fail('should have thrown as specified td < merge ttd for a post merge hardfork')
230+
} catch (error) {
231+
st.pass('throws error as specified td < merge ttd for a post merge hardfork')
232+
}
233+
234+
msg = 'should set HF correctly'
235+
236+
st.equal(c.setHardforkByBlockNumber(0), Hardfork.London, msg)
237+
st.equal(c.setHardforkByBlockNumber(1450409), Hardfork.London, msg)
238+
st.equal(c.setHardforkByBlockNumber(1450409, BigInt('17000000000000000')), Hardfork.Merge, msg)
239+
st.equal(c.setHardforkByBlockNumber(1735371), Hardfork.MergeForkIdTransition, msg)
240+
st.equal(
241+
c.setHardforkByBlockNumber(1735371, BigInt('17000000000000000')),
242+
Hardfork.MergeForkIdTransition,
243+
msg
244+
)
245+
try {
246+
st.equal(
247+
c.setHardforkByBlockNumber(1735371, BigInt('15000000000000000')),
248+
Hardfork.MergeForkIdTransition,
249+
msg
250+
)
251+
st.fail('should have thrown as specified td < merge ttd for a post merge hardfork')
252+
} catch (error) {
253+
st.pass('throws error as specified td < merge ttd for a post merge hardfork')
254+
}
255+
256+
// restore value
257+
mergeHf.block = prevMergeBlockVal
258+
259+
st.end()
260+
})
261+
262+
t.test(
263+
'should get correct merge and post merge hf with merge block specified ',
264+
function (st: tape.Test) {
265+
const c = new Common({ chain: Chain.Sepolia })
266+
267+
const mergeHf = c.hardforks().filter((hf) => hf.ttd !== undefined && hf.ttd !== null)[0]
268+
const prevMergeBlockVal = mergeHf.block
269+
// the terminal block on sepolia is 1450408
270+
mergeHf.block = 1450409
271+
const msg = 'should get HF correctly'
272+
273+
// should get merge even without td supplied as the merge hf now has the block specified
274+
st.equal(c.setHardforkByBlockNumber(1450409), Hardfork.Merge, msg)
275+
st.equal(
276+
c.setHardforkByBlockNumber(1450409, BigInt('17000000000000000')),
277+
Hardfork.Merge,
278+
msg
279+
)
280+
st.equal(c.setHardforkByBlockNumber(1735371), Hardfork.MergeForkIdTransition, msg)
281+
st.equal(
282+
c.setHardforkByBlockNumber(1735371, BigInt('17000000000000000')),
283+
Hardfork.MergeForkIdTransition,
284+
msg
285+
)
286+
// restore value
287+
mergeHf.block = prevMergeBlockVal
288+
289+
st.end()
290+
}
291+
)
292+
293+
t.test(
294+
'should throw if encounters a double ttd hardfork specification',
295+
function (st: tape.Test) {
296+
const c = new Common({ chain: Chain.Sepolia })
297+
// Add the ttd to mergeForkIdTransition which occurs post merge in sepolia
298+
c.hardforks().filter((hf) => hf.name === 'mergeForkIdTransition')[0]!['ttd'] =
299+
'17000000000000000'
300+
301+
try {
302+
c.setHardforkByBlockNumber(1735371)
303+
st.fail('should have thrown as two hardforks with ttd specified')
304+
} catch (error) {
305+
st.pass('throws error as two hardforks with ttd specified')
306+
}
307+
st.end()
308+
}
309+
)
190310
})

0 commit comments

Comments
 (0)