Skip to content

Commit 39638c8

Browse files
all: nuke total difficulty (#30744)
The total difficulty is the sum of all block difficulties from genesis to a certain block. This value was used in PoW for deciding which chain is heavier, and thus which chain to select. Since PoS has a different fork selection algorithm, all blocks since the merge have a difficulty of 0, and all total difficulties are the same for the past 2 years. Whilst the TDs are mostly useless nowadays, there was never really a reason to mess around removing them since they are so tiny. This reasoning changes when we go down the path of pruned chain history. In order to reconstruct any TD, we **must** retrieve all the headers from chain head to genesis and then iterate all the difficulties to compute the TD. In a world where we completely prune past chain segments (bodies, receipts, headers), it is not possible to reconstruct the TD at all. In a world where we still keep chain headers and prune only the rest, reconstructing it possible as long as we process (or download) the chain forward from genesis, but trying to snap sync the head first and backfill later hits the same issue, the TD becomes impossible to calculate until genesis is backfilled. All in all, the TD is a messy out-of-state, out-of-consensus computed field that is overall useless nowadays, but code relying on it forces the client into certain modes of operation and prevents other modes or other optimizations. This PR completely nukes out the TD from the node. It doesn't compute it, it doesn't operate on it, it's as if it didn't even exist. Caveats: - Whenever we have APIs that return TD (devp2p handshake, tracer, etc.) we return a TD of 0. - For era files, we recompute the TD during export time (fairly quick) to retain the format content. - It is not possible to "verify" the merge point (i.e. with TD gone, TTD is useless). Since we're not verifying PoW any more, just blindly trust it, not verifying but blindly trusting the many year old merge point seems just the same trust model. - Our tests still need to be able to generate pre and post merge blocks, so they need a new way to split the merge without TTD. The PR introduces a settable ttdBlock field on the consensus object which is used by tests as the block where originally the TTD happened. This is not needed for live nodes, we never want to generate old blocks. - One merge transition consensus test was disabled. With a non-operational TD, testing how the client reacts to TTD is useless, it cannot react. Questions: - Should we also drop total terminal difficulty from the genesis json? It's a number we cannot react on any more, so maybe it would be cleaner to get rid of even more concepts. --------- Co-authored-by: Gary Rong <garyrong0905@gmail.com>
1 parent 9516e0f commit 39638c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+177
-679
lines changed

cmd/devp2p/internal/ethtest/chain.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,7 @@ func (c *Chain) ForkID() forkid.ID {
143143
// TD calculates the total difficulty of the chain at the
144144
// chain head.
145145
func (c *Chain) TD() *big.Int {
146-
sum := new(big.Int)
147-
for _, block := range c.blocks[:c.Len()] {
148-
sum.Add(sum, block.Difficulty())
149-
}
150-
return sum
146+
return new(big.Int)
151147
}
152148

153149
// GetBlock returns the block at the specified number.

cmd/devp2p/internal/ethtest/suite.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package ethtest
1818

1919
import (
2020
"crypto/rand"
21-
"math/big"
2221
"reflect"
2322

2423
"github.com/ethereum/go-ethereum/common"
@@ -74,7 +73,6 @@ func (s *Suite) EthTests() []utesting.Test {
7473
{Name: "GetBlockBodies", Fn: s.TestGetBlockBodies},
7574
// // malicious handshakes + status
7675
{Name: "MaliciousHandshake", Fn: s.TestMaliciousHandshake},
77-
{Name: "MaliciousStatus", Fn: s.TestMaliciousStatus},
7876
// test transactions
7977
{Name: "LargeTxRequest", Fn: s.TestLargeTxRequest, Slow: true},
8078
{Name: "Transaction", Fn: s.TestTransaction},
@@ -453,42 +451,6 @@ func (s *Suite) TestMaliciousHandshake(t *utesting.T) {
453451
}
454452
}
455453

456-
func (s *Suite) TestMaliciousStatus(t *utesting.T) {
457-
t.Log(`This test sends a malicious eth Status message to the node and expects a disconnect.`)
458-
459-
conn, err := s.dial()
460-
if err != nil {
461-
t.Fatalf("dial failed: %v", err)
462-
}
463-
defer conn.Close()
464-
if err := conn.handshake(); err != nil {
465-
t.Fatalf("handshake failed: %v", err)
466-
}
467-
// Create status with large total difficulty.
468-
status := &eth.StatusPacket{
469-
ProtocolVersion: uint32(conn.negotiatedProtoVersion),
470-
NetworkID: s.chain.config.ChainID.Uint64(),
471-
TD: new(big.Int).SetBytes(randBuf(2048)),
472-
Head: s.chain.Head().Hash(),
473-
Genesis: s.chain.GetBlock(0).Hash(),
474-
ForkID: s.chain.ForkID(),
475-
}
476-
if err := conn.statusExchange(s.chain, status); err != nil {
477-
t.Fatalf("status exchange failed: %v", err)
478-
}
479-
// Wait for disconnect.
480-
code, _, err := conn.Read()
481-
if err != nil {
482-
t.Fatalf("error reading from connection: %v", err)
483-
}
484-
switch code {
485-
case discMsg:
486-
break
487-
default:
488-
t.Fatalf("expected disconnect, got: %d", code)
489-
}
490-
}
491-
492454
func (s *Suite) TestTransaction(t *utesting.T) {
493455
t.Log(`This test sends a valid transaction to the node and checks if the
494456
transaction gets propagated.`)

cmd/utils/cmd.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"errors"
2626
"fmt"
2727
"io"
28+
"math/big"
2829
"os"
2930
"os/signal"
3031
"path/filepath"
@@ -422,6 +423,10 @@ func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64) er
422423
buf = bytes.NewBuffer(nil)
423424
checksums []string
424425
)
426+
td := new(big.Int)
427+
for i := uint64(0); i < first; i++ {
428+
td.Add(td, bc.GetHeaderByNumber(i).Difficulty)
429+
}
425430
for i := first; i <= last; i += step {
426431
err := func() error {
427432
filename := filepath.Join(dir, era.Filename(network, int(i/step), common.Hash{}))
@@ -444,11 +449,8 @@ func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64) er
444449
if receipts == nil {
445450
return fmt.Errorf("export failed on #%d: receipts not found", n)
446451
}
447-
td := bc.GetTd(block.Hash(), block.NumberU64())
448-
if td == nil {
449-
return fmt.Errorf("export failed on #%d: total difficulty not found", n)
450-
}
451-
if err := w.Add(block, receipts, td); err != nil {
452+
td.Add(td, block.Difficulty())
453+
if err := w.Add(block, receipts, new(big.Int).Set(td)); err != nil {
452454
return err
453455
}
454456
}

consensus/beacon/consensus.go

Lines changed: 63 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ var (
6161
// is only used for necessary consensus checks. The legacy consensus engine can be any
6262
// engine implements the consensus interface (except the beacon itself).
6363
type Beacon struct {
64-
ethone consensus.Engine // Original consensus engine used in eth1, e.g. ethash or clique
64+
ethone consensus.Engine // Original consensus engine used in eth1, e.g. ethash or clique
65+
ttdblock *uint64 // Merge block-number for testchain generation without TTDs
6566
}
6667

6768
// New creates a consensus engine with the given embedded eth1 engine.
@@ -72,6 +73,18 @@ func New(ethone consensus.Engine) *Beacon {
7273
return &Beacon{ethone: ethone}
7374
}
7475

76+
// TestingTTDBlock is a replacement mechanism for TTD-based pre-/post-merge
77+
// splitting. With chain history deletion, TD calculations become impossible.
78+
// This is fine for progressing the live chain, but to be able to generate test
79+
// chains, we do need a split point. This method supports setting an explicit
80+
// block number to use as the splitter *for testing*, instead of having to keep
81+
// the notion of TDs in the client just for testing.
82+
//
83+
// The block with supplied number is regarded as the last pre-merge block.
84+
func (beacon *Beacon) TestingTTDBlock(number uint64) {
85+
beacon.ttdblock = &number
86+
}
87+
7588
// Author implements consensus.Engine, returning the verified author of the block.
7689
func (beacon *Beacon) Author(header *types.Header) (common.Address, error) {
7790
if !beacon.IsPoSHeader(header) {
@@ -83,78 +96,63 @@ func (beacon *Beacon) Author(header *types.Header) (common.Address, error) {
8396
// VerifyHeader checks whether a header conforms to the consensus rules of the
8497
// stock Ethereum consensus engine.
8598
func (beacon *Beacon) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error {
86-
reached, err := IsTTDReached(chain, header.ParentHash, header.Number.Uint64()-1)
87-
if err != nil {
88-
return err
89-
}
90-
if !reached {
91-
return beacon.ethone.VerifyHeader(chain, header)
92-
}
93-
// Short circuit if the parent is not known
99+
// During the live merge transition, the consensus engine used the terminal
100+
// total difficulty to detect when PoW (PoA) switched to PoS. Maintaining the
101+
// total difficulty values however require applying all the blocks from the
102+
// genesis to build up the TD. This stops being a possibility if the tail of
103+
// the chain is pruned already during sync.
104+
//
105+
// One heuristic that can be used to distinguish pre-merge and post-merge
106+
// blocks is whether their *difficulty* is >0 or ==0 respectively. This of
107+
// course would mean that we cannot prove anymore for a past chain that it
108+
// truly transitioned at the correct TTD, but if we consider that ancient
109+
// point in time finalized a long time ago, there should be no attempt from
110+
// the consensus client to rewrite very old history.
111+
//
112+
// One thing that's probably not needed but which we can add to make this
113+
// verification even stricter is to enforce that the chain can switch from
114+
// >0 to ==0 TD only once by forbidding an ==0 to be followed by a >0.
115+
116+
// Verify that we're not reverting to pre-merge from post-merge
94117
parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
95118
if parent == nil {
96119
return consensus.ErrUnknownAncestor
97120
}
98-
// Sanity checks passed, do a proper verification
99-
return beacon.verifyHeader(chain, header, parent)
100-
}
101-
102-
// errOut constructs an error channel with prefilled errors inside.
103-
func errOut(n int, err error) chan error {
104-
errs := make(chan error, n)
105-
for i := 0; i < n; i++ {
106-
errs <- err
121+
if parent.Difficulty.Sign() == 0 && header.Difficulty.Sign() > 0 {
122+
return consensus.ErrInvalidTerminalBlock
123+
}
124+
// Check >0 TDs with pre-merge, --0 TDs with post-merge rules
125+
if header.Difficulty.Sign() > 0 {
126+
return beacon.ethone.VerifyHeader(chain, header)
107127
}
108-
return errs
128+
return beacon.verifyHeader(chain, header, parent)
109129
}
110130

111131
// splitHeaders splits the provided header batch into two parts according to
112-
// the configured ttd. It requires the parent of header batch along with its
113-
// td are stored correctly in chain. If ttd is not configured yet, all headers
114-
// will be treated legacy PoW headers.
132+
// the difficulty field.
133+
//
115134
// Note, this function will not verify the header validity but just split them.
116-
func (beacon *Beacon) splitHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) ([]*types.Header, []*types.Header, error) {
117-
// TTD is not defined yet, all headers should be in legacy format.
118-
ttd := chain.Config().TerminalTotalDifficulty
119-
ptd := chain.GetTd(headers[0].ParentHash, headers[0].Number.Uint64()-1)
120-
if ptd == nil {
121-
return nil, nil, consensus.ErrUnknownAncestor
122-
}
123-
// The entire header batch already crosses the transition.
124-
if ptd.Cmp(ttd) >= 0 {
125-
return nil, headers, nil
126-
}
135+
func (beacon *Beacon) splitHeaders(headers []*types.Header) ([]*types.Header, []*types.Header) {
127136
var (
128137
preHeaders = headers
129138
postHeaders []*types.Header
130-
td = new(big.Int).Set(ptd)
131-
tdPassed bool
132139
)
133140
for i, header := range headers {
134-
if tdPassed {
141+
if header.Difficulty.Sign() == 0 {
135142
preHeaders = headers[:i]
136143
postHeaders = headers[i:]
137144
break
138145
}
139-
td = td.Add(td, header.Difficulty)
140-
if td.Cmp(ttd) >= 0 {
141-
// This is the last PoW header, it still belongs to
142-
// the preHeaders, so we cannot split+break yet.
143-
tdPassed = true
144-
}
145146
}
146-
return preHeaders, postHeaders, nil
147+
return preHeaders, postHeaders
147148
}
148149

149150
// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers
150151
// concurrently. The method returns a quit channel to abort the operations and
151152
// a results channel to retrieve the async verifications.
152153
// VerifyHeaders expect the headers to be ordered and continuous.
153154
func (beacon *Beacon) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) {
154-
preHeaders, postHeaders, err := beacon.splitHeaders(chain, headers)
155-
if err != nil {
156-
return make(chan struct{}), errOut(len(headers), err)
157-
}
155+
preHeaders, postHeaders := beacon.splitHeaders(headers)
158156
if len(postHeaders) == 0 {
159157
return beacon.ethone.VerifyHeaders(chain, headers)
160158
}
@@ -334,12 +332,15 @@ func (beacon *Beacon) verifyHeaders(chain consensus.ChainHeaderReader, headers [
334332
// Prepare implements consensus.Engine, initializing the difficulty field of a
335333
// header to conform to the beacon protocol. The changes are done inline.
336334
func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {
337-
// Transition isn't triggered yet, use the legacy rules for preparation.
338-
reached, err := IsTTDReached(chain, header.ParentHash, header.Number.Uint64()-1)
339-
if err != nil {
340-
return err
341-
}
342-
if !reached {
335+
// The beacon engine requires access to total difficulties to be able to
336+
// seal pre-merge and post-merge blocks. With the transition to removing
337+
// old blocks, TDs become unaccessible, thus making TTD based pre-/post-
338+
// merge decisions impossible.
339+
//
340+
// We do not need to seal non-merge blocks anymore live, but we do need
341+
// to be able to generate test chains, thus we're reverting to a testing-
342+
// settable field to direct that.
343+
if beacon.ttdblock != nil && *beacon.ttdblock >= header.Number.Uint64() {
343344
return beacon.ethone.Prepare(chain, header)
344345
}
345346
header.Difficulty = beaconDifficulty
@@ -449,8 +450,15 @@ func (beacon *Beacon) SealHash(header *types.Header) common.Hash {
449450
// the difficulty that a new block should have when created at time
450451
// given the parent block's time and difficulty.
451452
func (beacon *Beacon) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int {
452-
// Transition isn't triggered yet, use the legacy rules for calculation
453-
if reached, _ := IsTTDReached(chain, parent.Hash(), parent.Number.Uint64()); !reached {
453+
// The beacon engine requires access to total difficulties to be able to
454+
// seal pre-merge and post-merge blocks. With the transition to removing
455+
// old blocks, TDs become unaccessible, thus making TTD based pre-/post-
456+
// merge decisions impossible.
457+
//
458+
// We do not need to seal non-merge blocks anymore live, but we do need
459+
// to be able to generate test chains, thus we're reverting to a testing-
460+
// settable field to direct that.
461+
if beacon.ttdblock != nil && *beacon.ttdblock > parent.Number.Uint64() {
454462
return beacon.ethone.CalcDifficulty(chain, time, parent)
455463
}
456464
return beaconDifficulty
@@ -491,14 +499,3 @@ func (beacon *Beacon) SetThreads(threads int) {
491499
th.SetThreads(threads)
492500
}
493501
}
494-
495-
// IsTTDReached checks if the TotalTerminalDifficulty has been surpassed on the `parentHash` block.
496-
// It depends on the parentHash already being stored in the database.
497-
// If the parentHash is not stored in the database a UnknownAncestor error is returned.
498-
func IsTTDReached(chain consensus.ChainHeaderReader, parentHash common.Hash, parentNumber uint64) (bool, error) {
499-
td := chain.GetTd(parentHash, parentNumber)
500-
if td == nil {
501-
return false, consensus.ErrUnknownAncestor
502-
}
503-
return td.Cmp(chain.Config().TerminalTotalDifficulty) >= 0, nil
504-
}

consensus/beacon/faker.go

Lines changed: 0 additions & 41 deletions
This file was deleted.

consensus/consensus.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ type ChainHeaderReader interface {
4545

4646
// GetHeaderByHash retrieves a block header from the database by its hash.
4747
GetHeaderByHash(hash common.Hash) *types.Header
48-
49-
// GetTd retrieves the total difficulty from the database by hash and number.
50-
GetTd(hash common.Hash, number uint64) *big.Int
5148
}
5249

5350
// ChainReader defines a small collection of methods needed to access the local

core/bench_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,6 @@ func makeChainForBench(db ethdb.Database, genesis *Genesis, full bool, count uin
285285

286286
rawdb.WriteHeader(db, header)
287287
rawdb.WriteCanonicalHash(db, hash, n)
288-
rawdb.WriteTd(db, hash, n, big.NewInt(int64(n+1)))
289288

290289
if n == 0 {
291290
rawdb.WriteChainConfig(db, hash, genesis.Config)

0 commit comments

Comments
 (0)