Class: Reth::Synchronizer

Inherits:
Object
  • Object
show all
Defined in:
lib/reth/synchronizer.rb

Overview

Handles the synchronization of blocks.

There’s only one sync task active at a time. In order to deal with the worst case of initially syncing the wrong chain, a checkpoint blockhash can be specified and synced via force_sync.

Received blocks are given to chainservice.add_block, which has a fixed size queue, the synchronization blocks if the queue is full.

on_status:

if peer.head.chain_difficulty > chain.head.chain_difficulty
  fetch peer.head and handle as newblock

on_newblock:

if block.parent
  add
else
  sync

on_blocks/on_blockhashes:

if synctask
  handle to requester
elsif unkonwn and has parent
  add to chain
else
  drop

Constant Summary collapse

MAX_NEWBLOCK_AGE =

maximum age (in blocks) of blocks received as newblock

5

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(chainservice, force_sync = nil) ⇒ Synchronizer

Returns a new instance of Synchronizer.

Parameters:

  • force_sync (Array, NilClass) (defaults to: nil)

    If passed in array, it must be in the form of tuple: (blockhash, chain_difficulty). Helper for long initial syncs to get on the right chain used with first status_received.



45
46
47
48
49
50
51
# File 'lib/reth/synchronizer.rb', line 45

def initialize(chainservice, force_sync=nil)
  @chainservice = chainservice
  @force_sync = force_sync
  @chain = chainservice.chain
  @protocols = {} # proto => chain_difficulty
  @synctask = nil
end

Instance Attribute Details

#chainObject (readonly)

Returns the value of attribute chain.



37
38
39
# File 'lib/reth/synchronizer.rb', line 37

def chain
  @chain
end

#chainserviceObject (readonly)

Returns the value of attribute chainservice.



37
38
39
# File 'lib/reth/synchronizer.rb', line 37

def chainservice
  @chainservice
end

#synctaskObject (readonly)

Returns the value of attribute synctask.



37
38
39
# File 'lib/reth/synchronizer.rb', line 37

def synctask
  @synctask
end

Instance Method Details

#protocolsObject



62
63
64
65
66
67
68
69
# File 'lib/reth/synchronizer.rb', line 62

def protocols
  @protocols = @protocols
    .map {|proto, diff| [proto, diff] }
    .select {|tuple| !tuple[0].stopped? }
    .to_h

  @protocols.keys.sort_by {|proto| -@protocols[proto] }
end

#receive_blockhashes(proto, blockhashes) ⇒ Object



176
177
178
179
180
181
182
183
# File 'lib/reth/synchronizer.rb', line 176

def receive_blockhashes(proto, blockhashes)
  logger.debug 'blockhashes received', proto: proto, num: blockhashes.size
  if @synctask
    @synctask.receive_blockhashes proto, blockhashes
  else
    logger.warn 'no synctask, not expecting blockhashes'
  end
end

#receive_blocks(proto, t_blocks) ⇒ Object



167
168
169
170
171
172
173
174
# File 'lib/reth/synchronizer.rb', line 167

def receive_blocks(proto, t_blocks)
  logger.debug 'blocks received', proto: proto, num: t_blocks.size
  if @synctask
    @synctask.receive_blocks proto, t_blocks
  else
    logger.warn 'no synctask, not expecting blocks'
  end
end

#receive_newblock(proto, t_block, chain_difficulty) ⇒ Object

Called if there’s a newblock announced on the network.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/reth/synchronizer.rb', line 74

def receive_newblock(proto, t_block, chain_difficulty)
  logger.debug 'newblock', proto: proto, block: t_block, chain_difficulty: chain_difficulty, client: proto.peer.remote_client_version

  if @chain.include?(t_block.header.full_hash)
    raise AssertError, 'chain difficulty mismatch' unless chain_difficulty == @chain.get(t_block.header.full_hash).chain_difficulty
  end

  @protocols[proto] = chain_difficulty

  if @chainservice.knows_block(t_block.header.full_hash)
    logger.debug 'known block'
    return
  end

  expected_difficulty = @chain.head.chain_difficulty + t_block.header.difficulty
  if chain_difficulty >= @chain.head.chain_difficulty
    # broadcast duplicates filtering is done in chainservice
    logger.debug 'sufficient difficulty, broadcasting', client: proto.peer.remote_client_version
    @chainservice.broadcast_newblock t_block, chain_difficulty, proto
  else
    age = @chain.head.number - t_block.header.number
    logger.debug "low difficulty", client: proto.peer.remote_client_version, chain_difficulty: chain_difficulty, expected_difficulty: expected_difficulty, block_age: age

    if age > MAX_NEWBLOCK_AGE
      logger.debug 'newblock is too old, not adding', block_age: age, max_age: MAX_NEWBLOCK_AGE
      return
    end
  end

  if @chainservice.knows_block(t_block.header.prevhash)
    logger.debug 'adding block'
    @chainservice.add_block t_block, proto
  else
    logger.debug 'missing parent'
    if @synctask
      logger.debug 'existing task, discarding'
    else
      @synctask = SyncTask.new self, proto, t_block.header.full_hash, chain_difficulty
    end
  end
end

#receive_newblockhashes(proto, newblockhashes) ⇒ Object

No way to check if this really an interesting block at this point. Might lead to an amplification attack, need to track this proto and judge usefulness.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/reth/synchronizer.rb', line 147

def receive_newblockhashes(proto, newblockhashes)
  logger.debug 'received newblockhashes', num: newblockhashes.size, proto: proto

  newblockhashes = newblockhashes.select {|h| !@chainservice.knows_block(h) }

  known = @protocols.include?(proto)
  if !known || newblockhashes.empty? || @synctask
    logger.debug 'discarding', known: known, synctask: syncing?, num: newblockhashes.size
    return
  end

  if newblockhashes.size != 1
    logger.warn 'supporting only one newblockhash', num: newblockhashes.size
  end
  blockhash = newblockhashes[0]

  logger.debug 'starting synctask for newblockhashes', blockhash: Utils.encode_hex(blockhash)
  @synctask = SyncTask.new self, proto, blockhash, 0, true
end

#receive_status(proto, blockhash, chain_difficulty) ⇒ Object

Called if a new peer is connected.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/reth/synchronizer.rb', line 119

def receive_status(proto, blockhash, chain_difficulty)
  logger.debug 'status received', proto: proto, chain_difficulty: chain_difficulty

  @protocols[proto] = chain_difficulty

  if @chainservice.knows_block(blockhash) || @synctask
    logger.debug 'existing task or known hash, discarding'
    return
  end

  if @force_sync
    blockhash, difficulty = force_sync
    logger.debug 'starting forced synctask', blockhash: Utils.encode_hex(blockhash)
    @synctask = SyncTask.new self, proto, blockhash, difficulty
  elsif chain_difficulty > @chain.head.chain_difficulty
    logger.debug 'sufficient difficulty'
    @synctask = SyncTask.new self, proto, blockhash, chain_difficulty
  end
rescue
  logger.debug $!
  logger.debug $!.backtrace[0,10].join("\n")
end

#syncing?Boolean

Returns:

  • (Boolean)


53
54
55
# File 'lib/reth/synchronizer.rb', line 53

def syncing?
  !!@synctask
end

#synctask_exited(success = false) ⇒ Object



57
58
59
60
# File 'lib/reth/synchronizer.rb', line 57

def synctask_exited(success=false)
  @force_sync = nil if success
  @synctask = nil
end