Skip to content

Commit 53cf668

Browse files
committed
chore: add new chainsync
1 parent ba7695d commit 53cf668

File tree

14 files changed

+2282
-0
lines changed

14 files changed

+2282
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package chainsegment
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"math/big"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
12+
"github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/client"
13+
)
14+
15+
const MaxNumPollBlocks = 50
16+
17+
var (
18+
ErrReorg = errors.New("detected reorg in updated chain-segment")
19+
ErrEmpty = errors.New("empty chain-segment")
20+
ErrUpdateBlockTooFarInPast = errors.New("the updated block reaches too far in the past for the chain-segment")
21+
ErrOverlapTooBig = errors.New("chain-segment overlap too big")
22+
)
23+
24+
type UpdateLatestResult struct {
25+
// the full new segment with the reorg applied
26+
FullSegment *ChainSegment
27+
// the removed segment that is not part of the new full segment anymore
28+
// (reorged blocks)
29+
RemovedSegment *ChainSegment
30+
// the updated segment of new blocks that were not part of the old chain
31+
// (new blocks including the replacement blocks from a reorg)
32+
UpdatedSegment *ChainSegment
33+
}
34+
35+
// capNumPollBlocks is a pipeline function
36+
// that restricts the number of blocks to be
37+
// polled, e.g. during filling gaps between
38+
// two chain-segments.
39+
func capNumPollBlocks(num int) int {
40+
if num > MaxNumPollBlocks {
41+
return MaxNumPollBlocks
42+
} else if num < 1 {
43+
return 1
44+
}
45+
return num
46+
}
47+
48+
type ChainSegment struct {
49+
chain []*types.Header
50+
}
51+
52+
func NewChainSegment(chain ...*types.Header) *ChainSegment {
53+
bc := &ChainSegment{
54+
chain: chain,
55+
}
56+
return bc
57+
}
58+
59+
func (bc *ChainSegment) GetHeaderByHash(h common.Hash) *types.Header {
60+
// OPTIM: this should be implemented more efficiently
61+
// with a hash-map
62+
for _, header := range bc.chain {
63+
if header.Hash().Cmp(h) == 0 {
64+
return header
65+
}
66+
}
67+
return nil
68+
}
69+
70+
func (bc *ChainSegment) Len() int {
71+
return len(bc.chain)
72+
}
73+
74+
func (bc *ChainSegment) Earliest() *types.Header {
75+
if len(bc.chain) == 0 {
76+
return nil
77+
}
78+
return bc.chain[0]
79+
}
80+
81+
func (bc *ChainSegment) Latest() *types.Header {
82+
if len(bc.chain) == 0 {
83+
return nil
84+
}
85+
return bc.chain[len(bc.chain)-1]
86+
}
87+
88+
func (bc *ChainSegment) Get() []*types.Header {
89+
return bc.chain
90+
}
91+
92+
func (bc *ChainSegment) Copy() *ChainSegment {
93+
return NewChainSegment(bc.chain...)
94+
}
95+
96+
// UpdateLatest incorporates a new chainsegment `update` into it's existing
97+
// chain-segment.
98+
// For this it backtracks the new chain-segment until it finds the common ancestor
99+
// with it's current chain-segment. If there is no ancestor because of a block-number
100+
// gap between the old segments "latest" block and the new segments "earliest" block,
101+
// it will incrementally batch-augment the 'update' chain-segment with blocks older than
102+
// it's "earliest" block, and call the UpdateLatest latest method recursively
103+
// until the algorithm finds a common ancestor.
104+
// The outcome of this process is an `UpdateLatestResult`, which
105+
// communicates to the caller what part of the previous chain-segment had to be removed,
106+
// and what part of the `update` chain-segment was appended to the previous chain-segment
107+
// after removal of out-of-date blocks, in addition to the full newly updated chain-segment.
108+
// This is a pointer method that updates the internal state of it's chain-segment!
109+
func (bc *ChainSegment) UpdateLatest(ctx context.Context, c client.Sync, update *ChainSegment) (UpdateLatestResult, error) {
110+
update = update.Copy()
111+
if bc.Len() == 0 {
112+
// We can't compare anything - instead of silently absorbing the
113+
// whole new segment, communicate this to the caller with a specific error.
114+
return UpdateLatestResult{}, ErrEmpty
115+
}
116+
117+
if bc.Earliest().Number.Cmp(update.Earliest().Number) == 1 {
118+
// We don't reach so far in the past for the old chain-segment.
119+
// This happens when there is a large reorg, while the chain-segment
120+
// of the cache is still small.
121+
return UpdateLatestResult{}, fmt.Errorf(
122+
"segment earliest=%d, update earliest=%d: %w",
123+
bc.Earliest().Number.Int64(), update.Earliest().Number.Int64(),
124+
ErrUpdateBlockTooFarInPast,
125+
)
126+
}
127+
overlapBig := new(big.Int).Add(
128+
new(big.Int).Sub(bc.Latest().Number, update.Earliest().Number),
129+
// both being the same height means one block overlap, so add 1
130+
big.NewInt(1),
131+
)
132+
if !overlapBig.IsInt64() {
133+
// this should never happen, this would be too large of a gap
134+
return UpdateLatestResult{}, ErrOverlapTooBig
135+
}
136+
137+
overlap := int(overlapBig.Int64())
138+
if overlap < 0 {
139+
// overlap is negative, this means we have a gap:
140+
extendedUpdate, err := update.ExtendLeft(ctx, c, capNumPollBlocks(-overlap))
141+
if err != nil {
142+
return UpdateLatestResult{}, fmt.Errorf("failed to extend left gap: %w", err)
143+
}
144+
return bc.UpdateLatest(ctx, c, extendedUpdate)
145+
} else if overlap == 0 {
146+
if update.Earliest().ParentHash.Cmp(bc.Latest().Hash()) == 0 {
147+
// the new segment extends the old one perfectly
148+
return UpdateLatestResult{
149+
FullSegment: bc.Copy().AddRight(update),
150+
RemovedSegment: nil,
151+
UpdatedSegment: update,
152+
}, nil
153+
}
154+
// the block-numbers align, but the new segment
155+
// seems to be from a reorg that branches off within the old segment
156+
_, err := update.ExtendLeft(ctx, c, capNumPollBlocks(bc.Len()))
157+
if err != nil {
158+
return UpdateLatestResult{}, fmt.Errorf("failed to extend into reorg: %w", err)
159+
}
160+
return bc.UpdateLatest(ctx, c, update)
161+
}
162+
// implicit case - overlap > 0:
163+
// now we can compare the segments and find the common ancestor
164+
// Return the segment of the overlap from the current segment
165+
// and compute the diff of the whole new update segment.
166+
removed, updated := bc.GetLatest(overlap).DiffLeftAligned(update)
167+
// don't copy, but use the method's struct,
168+
// that way we modify in-place
169+
full := bc
170+
if removed != nil {
171+
// cut the reorged section that has to be removed
172+
// so that we only have the "left" section up until the
173+
// common ancestor
174+
full = full.GetEarliest(full.Len() - removed.Len())
175+
}
176+
if updated != nil {
177+
// and now append the update section
178+
// to the right, effectively removing the reorged section
179+
full.AddRight(updated)
180+
}
181+
return UpdateLatestResult{
182+
FullSegment: full,
183+
RemovedSegment: removed,
184+
UpdatedSegment: updated,
185+
}, nil
186+
}
187+
188+
// AddRight adds the `add` chain-segment to the "right" of the
189+
// original chain-segment, and thus assumes that the `add` segments
190+
// Earliest() block is the child-block of the original segments
191+
// Latest() block. This condition is *not* checked,
192+
// so callers have to guarantee for it.
193+
func (bc *ChainSegment) AddRight(add *ChainSegment) *ChainSegment {
194+
bc.chain = append(bc.chain, add.chain...)
195+
return bc
196+
}
197+
198+
// DiffLeftAligned compares the ChainSegment to another chain-segment that
199+
// starts at the same Earliest() block-number.
200+
// It walks both segments from earliest to latest header simultaneously
201+
// and compares the block-hashes. As soon as there is a mismatch
202+
// in block-hashes, a consecutive difference from that point on is assumed.
203+
// All diff blocks from the `other` chain-segment will be appended to the returned `update`
204+
// chain-segment, and all diff blocks from the original chain-segment
205+
// will be appended to the `remove` chain-segment.
206+
// If there is no overlap in the diff, but the `other` chain-segment is longer than
207+
// the original segment, the `remove` segment will be nil, and the `update` segment
208+
// will consist of the non-overlapping blocks of the `other` segment.
209+
// If both segments are identical, both `update` and `remove` segments will be nil.
210+
func (bc *ChainSegment) DiffLeftAligned(other *ChainSegment) (remove, update *ChainSegment) {
211+
// 1) assumes both segments start at the same block height (earliest block at index 0 with same blocknum)
212+
// 2) assumes the other.Len() >= bc.Len()
213+
214+
// Compare the two and see if we have to reorg based on the hashes
215+
removed := []*types.Header{}
216+
updated := []*types.Header{}
217+
oldChain := bc.Get()
218+
newChain := other.Get()
219+
220+
for i := 0; i < len(newChain); i++ {
221+
var oldHeader *types.Header
222+
newHeader := newChain[i]
223+
if len(oldChain) > i {
224+
oldHeader = oldChain[i]
225+
}
226+
if oldHeader == nil {
227+
updated = append(updated, newHeader)
228+
// TODO: sanity check also the blocknum + parent hash chain
229+
// so that we are sure that we have consecutive segments.
230+
} else if oldHeader.Hash().Cmp(newHeader.Hash()) != 0 {
231+
removed = append(removed, oldHeader)
232+
updated = append(updated, newHeader)
233+
}
234+
}
235+
var removedSegment, updatedSegment *ChainSegment
236+
if len(removed) > 0 {
237+
removedSegment = NewChainSegment(removed...)
238+
}
239+
if len(updated) > 0 {
240+
updatedSegment = NewChainSegment(updated...)
241+
}
242+
return removedSegment, updatedSegment
243+
}
244+
245+
// GetLatest retrieves the "n" latest blocks from this
246+
// ChainSegment.
247+
// If the segment is shorter than n, the whole segment gets returned.
248+
func (bc *ChainSegment) GetLatest(n int) *ChainSegment {
249+
if n > bc.Len() {
250+
n = bc.Len()
251+
}
252+
return NewChainSegment(bc.chain[len(bc.chain)-n : len(bc.chain)]...)
253+
}
254+
255+
// GetLatest retrieves the "n" earliest blocks from this
256+
// ChainSegment.
257+
// If the segment is shorter than n, the whole segment gets returned.
258+
func (bc *ChainSegment) GetEarliest(n int) *ChainSegment {
259+
if n > bc.Len() {
260+
n = bc.Len()
261+
}
262+
return NewChainSegment(bc.chain[:n]...)
263+
}
264+
265+
func (bc *ChainSegment) NewSegmentRight(ctx context.Context, c client.Sync, num int) (*ChainSegment, error) {
266+
rightMost := bc.Latest()
267+
if rightMost == nil {
268+
return nil, ErrEmpty
269+
}
270+
chain := []*types.Header{}
271+
previous := rightMost
272+
for i := 1; i <= num; i++ {
273+
blockNum := new(big.Int).Sub(rightMost.Number, big.NewInt(int64(i)))
274+
h, err := c.HeaderByNumber(ctx, blockNum)
275+
if err != nil {
276+
return nil, err
277+
}
278+
if h.Hash().Cmp(previous.ParentHash) != 0 {
279+
// the server has a different chain state than this segment,
280+
// so it is part of a reorged away chain-segment
281+
return nil, ErrReorg
282+
}
283+
chain = append(chain, h)
284+
previous = h
285+
}
286+
return NewChainSegment(chain...), nil
287+
}
288+
289+
func (bc *ChainSegment) ExtendLeft(ctx context.Context, c client.Sync, num int) (*ChainSegment, error) {
290+
leftMost := bc.Earliest()
291+
if leftMost == nil {
292+
return nil, ErrEmpty
293+
}
294+
for num > 0 {
295+
blockNum := new(big.Int).Sub(leftMost.Number, big.NewInt(int64(1)))
296+
//OPTIM: we do cap the max poll number when calling this method,
297+
// but then we make one request per block anyways.
298+
// This doesn't make sense, but there currently is no batching
299+
// for retrieving ranges of headers.
300+
h, err := c.HeaderByNumber(ctx, blockNum)
301+
if err != nil {
302+
return nil, fmt.Errorf("failed to retrieve header by number (#%d): %w", blockNum.Uint64(), err)
303+
}
304+
if h.Hash().Cmp(leftMost.ParentHash) != 0 {
305+
// The server has a different chain state than this segment,
306+
// so it is part of a reorged away chain-segment.
307+
// This can also happen when the server reorged during this loop
308+
// and we now polled the parent with an unexpected hash.
309+
return nil, ErrReorg
310+
}
311+
bc.chain = append([]*types.Header{h}, bc.chain...)
312+
leftMost = h
313+
num--
314+
}
315+
return bc, nil
316+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package chainsync
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/core/types"
9+
10+
"github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/syncer"
11+
"github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service"
12+
)
13+
14+
type Chainsync struct {
15+
options *options
16+
fetcher *syncer.Fetcher
17+
}
18+
19+
func New(options ...Option) (*Chainsync, error) {
20+
opts := defaultOptions()
21+
for _, o := range options {
22+
if err := o(opts); err != nil {
23+
return nil, fmt.Errorf("error applying option to Chainsync: %w", err)
24+
}
25+
}
26+
27+
if err := opts.verify(); err != nil {
28+
return nil, fmt.Errorf("error verifying options to Chainsync: %w", err)
29+
}
30+
return &Chainsync{
31+
options: opts,
32+
}, nil
33+
}
34+
35+
func (c *Chainsync) Start(ctx context.Context, runner service.Runner) error {
36+
var err error
37+
c.fetcher, err = c.options.initFetcher(ctx)
38+
if err != nil {
39+
return fmt.Errorf("error initializing Chainsync: %w", err)
40+
}
41+
return c.fetcher.Start(ctx, runner)
42+
}
43+
44+
func (c *Chainsync) GetHeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) {
45+
return c.fetcher.GetHeaderByHash(ctx, h)
46+
}

0 commit comments

Comments
 (0)