Skip to content

Commit b7daeac

Browse files
committed
Choose better header timestamp, if none supplied
Actually look at the clock, 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 099aa55 commit b7daeac

File tree

10 files changed

+102
-13
lines changed

10 files changed

+102
-13
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,
@@ -25,6 +26,31 @@
2526
)
2627

2728

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

eth/abc.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4070,6 +4070,30 @@ def __init__(self, base_db: AtomicDatabaseAPI, header: BlockHeaderAPI = None) ->
40704070
"""
40714071
...
40724072

4073+
@abstractmethod
4074+
def set_header_timestamp(self, timestamp: int) -> None:
4075+
"""
4076+
Set the timestamp of the pending header to mine.
4077+
4078+
This is mostly useful for testing, as the timestamp will be chosen
4079+
automatically if this method is not called.
4080+
"""
4081+
...
4082+
4083+
@abstractmethod
4084+
def set_coinbase(self, coinbase: Address) -> None:
4085+
"""
4086+
Set the coinbase for all new headers.
4087+
"""
4088+
...
4089+
4090+
@abstractmethod
4091+
def set_extra_data(self, extra_data: bytes) -> None:
4092+
"""
4093+
Set the extra_data (sometimes called graffiti) for all new headers.
4094+
"""
4095+
...
4096+
40734097
@abstractmethod
40744098
def mine_all(
40754099
self,

eth/chains/base.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@
6060
)
6161
from eth.constants import (
6262
EMPTY_UNCLE_HASH,
63+
GENESIS_EXTRA_DATA,
6364
MAX_UNCLE_DEPTH,
65+
ZERO_ADDRESS,
6466
)
6567

6668
from eth.db.chain import (
@@ -646,6 +648,8 @@ class MiningChain(Chain, MiningChainAPI):
646648

647649
def __init__(self, base_db: AtomicDatabaseAPI, header: BlockHeaderAPI = None) -> None:
648650
super().__init__(base_db)
651+
self._header_extra_data = GENESIS_EXTRA_DATA
652+
self._header_coinbase = ZERO_ADDRESS
649653
self.header = self.ensure_header(header)
650654

651655
def apply_transaction(self,
@@ -680,6 +684,17 @@ def import_block(self,
680684
self.header = self.ensure_header()
681685
return result
682686

687+
def set_header_timestamp(self, timestamp: int) -> None:
688+
self.header = self.header.copy(timestamp=timestamp)
689+
690+
def set_coinbase(self, coinbase: Address) -> None:
691+
self.header = self.header.copy(coinbase=coinbase)
692+
self._header_coinbase = coinbase
693+
694+
def set_extra_data(self, extra_data: bytes) -> None:
695+
self.header = self.header.copy(extra_data=extra_data)
696+
self._header_extra_data = extra_data
697+
683698
def mine_all(
684699
self,
685700
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
Optional,
43
cast,
@@ -24,6 +23,9 @@
2423
encode_hex,
2524
)
2625

26+
from eth._utils.headers import (
27+
new_timestamp_from_parent,
28+
)
2729
from eth.abc import (
2830
BlockHeaderAPI,
2931
MiningHeaderAPI,
@@ -123,7 +125,12 @@ def __init__(self, # type: ignore # noqa: F811
123125
mix_hash: Hash32 = ZERO_HASH32,
124126
nonce: bytes = GENESIS_NONCE) -> None:
125127
if timestamp is None:
126-
timestamp = int(time.time())
128+
if parent_hash == ZERO_HASH32:
129+
timestamp = new_timestamp_from_parent(None)
130+
else:
131+
# without access to the parent header, we cannot select a new timestamp correctly
132+
raise ValueError("Must set timestamp explicitly if this is not a genesis header")
133+
127134
super().__init__(
128135
parent_hash=parent_hash,
129136
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/headers.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from eth._utils.headers import (
1111
compute_gas_limit,
1212
fill_header_params_from_parent,
13+
new_timestamp_from_parent,
1314
)
1415
from eth.abc import (
1516
BlockHeaderAPI,
@@ -79,11 +80,7 @@ def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int
7980

8081
# byzantium
8182
if 'timestamp' not in header_params:
82-
if parent_header is None:
83-
timestamp = 0
84-
else:
85-
timestamp = parent_header.timestamp + 1
86-
header_params['timestamp'] = timestamp
83+
header_params['timestamp'] = new_timestamp_from_parent(parent_header)
8784

8885
if 'difficulty' not in header_params:
8986
if parent_header is None:

tests/database/test_eth1_chaindb.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,13 @@ def test_chaindb_get_score(chaindb):
275275
assert genesis_score == 1
276276
assert chaindb.get_score(genesis.hash) == 1
277277

278-
block1 = BlockHeader(difficulty=10, block_number=1, gas_limit=0, parent_hash=genesis.hash)
278+
block1 = BlockHeader(
279+
difficulty=10,
280+
block_number=1,
281+
gas_limit=0,
282+
parent_hash=genesis.hash,
283+
timestamp=genesis.timestamp + 1,
284+
)
279285
chaindb.persist_header(block1)
280286

281287
block1_score_key = SchemaV1.make_block_hash_to_score_lookup_key(block1.hash)

0 commit comments

Comments
 (0)