Skip to content

Commit ba7cf8d

Browse files
committed
Choose correct header timestamp, if none supplied
Actually look at the clock, in UTC, and compare it to the parent. Related: - Fix doctests: set timestamp manually for reproducible results (new MiningChain API) - clique should verify timestamp in UTC (bug uncovered) - Only guess timestamp on non-genesis headers (when parent & VM are unavailable), so manually set in a test where headers are built manually
1 parent da79f10 commit ba7cf8d

File tree

12 files changed

+85
-15
lines changed

12 files changed

+85
-15
lines changed

docs/guides/understanding_the_mining_process.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ zero value transfer transaction.
341341
... ))
342342

343343
>>> chain = klass.from_genesis(AtomicDB(), GENESIS_PARAMS)
344+
>>> genesis = chain.get_canonical_block_header_by_number(0)
344345
>>> vm = chain.get_vm()
345346

346347
>>> nonce = vm.state.get_nonce(SENDER)
@@ -358,6 +359,12 @@ zero value transfer transaction.
358359

359360
>>> chain.apply_transaction(signed_tx)
360361
(<ByzantiumBlock(#Block #1...)
362+
363+
>>> # Normally, we can let the timestamp be chosen automatically, but
364+
>>> # for the sake of reproducing exactly the same block every time,
365+
>>> # we will set it manually here:
366+
>>> chain.set_header_timestamp(genesis.timestamp + 1)
367+
361368
>>> # We have to finalize the block first in order to be able read the
362369
>>> # attributes that are important for the PoW algorithm
363370
>>> block_result = chain.get_vm().finalize_block(chain.get_block())

eth/_utils/headers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from typing import (
23
Dict,
34
Tuple,
@@ -26,6 +27,31 @@
2627
)
2728

2829

30+
def eth_now() -> int:
31+
"""
32+
The timestamp is in UTC.
33+
"""
34+
return int(datetime.datetime.utcnow().timestamp())
35+
36+
37+
def new_timestamp_from_parent(parent: BlockHeaderAPI) -> int:
38+
"""
39+
Generate a timestamp to use on a new header.
40+
41+
Generally, attempt to use the current time. If timestamp is too old (equal
42+
or less than parent), return `parent.timestamp + 1`. If parent is None,
43+
then consider this a genesis block.
44+
"""
45+
if parent is None:
46+
return eth_now()
47+
else:
48+
# header timestamps must increment
49+
return max(
50+
parent.timestamp + 1,
51+
eth_now(),
52+
)
53+
54+
2955
def fill_header_params_from_parent(
3056
parent: BlockHeaderAPI,
3157
gas_limit: int,

eth/abc.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4054,6 +4054,16 @@ def __init__(self, base_db: AtomicDatabaseAPI, header: BlockHeaderAPI = None) ->
40544054
"""
40554055
...
40564056

4057+
@abstractmethod
4058+
def set_header_timestamp(self, timestamp: int) -> None:
4059+
"""
4060+
Set the timestamp of the pending header to mine.
4061+
4062+
This is mostly useful for testing, as the timestamp will be chosen
4063+
automatically if this method is not called.
4064+
"""
4065+
...
4066+
40574067
@abstractmethod
40584068
def mine_all(
40594069
self,

eth/chains/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,9 @@ def import_block(self,
660660
self.header = self.ensure_header()
661661
return result
662662

663+
def set_header_timestamp(self, timestamp: int) -> None:
664+
self.header = self.header.copy(timestamp=timestamp)
665+
663666
def mine_all(
664667
self,
665668
transactions: Sequence[SignedTransactionAPI],

eth/consensus/clique/_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import time
21
from typing import (
32
Iterable,
43
)
@@ -15,6 +14,7 @@
1514
ValidationError,
1615
)
1716

17+
from eth._utils.headers import eth_now
1818
from eth.abc import (
1919
BlockHeaderAPI,
2020
)
@@ -127,7 +127,7 @@ def is_checkpoint(block_number: int, epoch_length: int) -> bool:
127127

128128
def validate_header_integrity(header: BlockHeaderAPI, epoch_length: int) -> None:
129129

130-
if header.timestamp > int(time.time()):
130+
if header.timestamp > eth_now():
131131
raise ValidationError(f"Invalid future timestamp: {header.timestamp}")
132132

133133
at_checkpoint = is_checkpoint(header.block_number, epoch_length)

eth/rlp/headers.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import time
21
from typing import (
32
cast,
43
overload,
@@ -23,6 +22,9 @@
2322
encode_hex,
2423
)
2524

25+
from eth._utils.headers import (
26+
new_timestamp_from_parent,
27+
)
2628
from eth.abc import (
2729
BlockHeaderAPI,
2830
MiningHeaderAPI,
@@ -122,7 +124,12 @@ def __init__(self, # type: ignore # noqa: F811
122124
mix_hash: Hash32 = ZERO_HASH32,
123125
nonce: bytes = GENESIS_NONCE) -> None:
124126
if timestamp is None:
125-
timestamp = int(time.time())
127+
if parent_hash == ZERO_HASH32:
128+
timestamp = new_timestamp_from_parent(None)
129+
else:
130+
# without access to the parent header, we cannot select a new timestamp correctly
131+
raise ValueError("Must set timestamp explicitly if this is not a genesis header")
132+
126133
super().__init__(
127134
parent_hash=parent_hash,
128135
uncles_hash=uncles_hash,

eth/vm/forks/byzantium/headers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
from eth._utils.db import (
2121
get_parent_header,
2222
)
23+
from eth._utils.headers import (
24+
new_timestamp_from_parent,
25+
)
2326
from eth.validation import (
2427
validate_gt,
2528
validate_header_params_for_configuration,
@@ -77,9 +80,10 @@ def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int
7780
parent_header: BlockHeaderAPI,
7881
**header_params: Any) -> BlockHeaderAPI:
7982

80-
if 'difficulty' not in header_params:
81-
header_params.setdefault('timestamp', parent_header.timestamp + 1)
83+
if 'timestamp' not in header_params:
84+
header_params['timestamp'] = new_timestamp_from_parent(parent_header)
8285

86+
if 'difficulty' not in header_params:
8387
header_params['difficulty'] = difficulty_fn(
8488
parent_header,
8589
header_params['timestamp'],

eth/vm/forks/frontier/headers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from eth._utils.headers import (
2323
compute_gas_limit,
2424
fill_header_params_from_parent,
25+
new_timestamp_from_parent,
2526
)
2627
from eth.rlp.headers import BlockHeader
2728

@@ -75,10 +76,12 @@ def compute_frontier_difficulty(parent_header: BlockHeaderAPI, timestamp: int) -
7576

7677
def create_frontier_header_from_parent(parent_header: BlockHeaderAPI,
7778
**header_params: Any) -> BlockHeader:
79+
if 'timestamp' not in header_params:
80+
header_params['timestamp'] = new_timestamp_from_parent(parent_header)
81+
7882
if 'difficulty' not in header_params:
7983
# Use setdefault to ensure the new header has the same timestamp we use to calculate its
8084
# difficulty.
81-
header_params.setdefault('timestamp', parent_header.timestamp + 1)
8285
header_params['difficulty'] = compute_frontier_difficulty(
8386
parent_header,
8487
header_params['timestamp'],

eth/vm/forks/london/blocks.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import time
21
from typing import (
32
List,
43
Type,
@@ -24,6 +23,9 @@
2423
binary
2524
)
2625

26+
from eth._utils.headers import (
27+
new_timestamp_from_parent,
28+
)
2729
from eth.abc import (
2830
BlockHeaderAPI,
2931
BlockHeaderSedesAPI,
@@ -105,7 +107,11 @@ def __init__(self,
105107
nonce: bytes = GENESIS_NONCE,
106108
base_fee_per_gas: int = 0) -> None:
107109
if timestamp is None:
108-
timestamp = int(time.time())
110+
if parent_hash == ZERO_HASH32:
111+
timestamp = new_timestamp_from_parent(None)
112+
else:
113+
# without access to the parent header, we cannot select a new timestamp correctly
114+
raise ValueError("Must set timestamp explicitly if this is not a genesis header")
109115
super().__init__(
110116
parent_hash=parent_hash,
111117
uncles_hash=uncles_hash,

eth/vm/forks/london/headers.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from eth._utils.headers import (
1414
compute_gas_limit,
1515
fill_header_params_from_parent,
16+
new_timestamp_from_parent,
1617
)
1718
from eth.abc import (
1819
BlockHeaderAPI,
@@ -90,11 +91,7 @@ def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int
9091

9192
# byzantium
9293
if 'timestamp' not in header_params:
93-
if parent_header is None:
94-
timestamp = 0
95-
else:
96-
timestamp = parent_header.timestamp + 1
97-
header_params['timestamp'] = timestamp
94+
header_params['timestamp'] = new_timestamp_from_parent(parent_header)
9895

9996
if 'difficulty' not in header_params:
10097
if parent_header is None:

0 commit comments

Comments
 (0)