Skip to content

Commit 64c7533

Browse files
authored
refactor: extract VerifyRange and remove verification out of store (#268)
Removes verification out of the Store => requires local exchange to do verification => extracts canonical VerifyRange with unit testing => uncovers minor edge cases with range verification Closes #266
1 parent b2616fd commit 64c7533

File tree

7 files changed

+156
-143
lines changed

7 files changed

+156
-143
lines changed

headertest/verify_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,96 @@ func TestVerify(t *testing.T) {
125125
})
126126
}
127127
}
128+
129+
func TestVerifyRange(t *testing.T) {
130+
tests := []struct {
131+
name string
132+
setup func(*DummySuite) (*DummyHeader, []*DummyHeader)
133+
err error
134+
verified int // number of headers that should be verified before error
135+
}{
136+
{
137+
name: "successful verification of all headers",
138+
setup: func(suite *DummySuite) (*DummyHeader, []*DummyHeader) {
139+
trusted := suite.GenDummyHeaders(1)[0]
140+
untrstd := suite.GenDummyHeaders(5)
141+
return trusted, untrstd
142+
},
143+
verified: 5,
144+
},
145+
{
146+
name: "empty untrusted headers range",
147+
setup: func(suite *DummySuite) (*DummyHeader, []*DummyHeader) {
148+
trusted := suite.GenDummyHeaders(1)[0]
149+
return trusted, []*DummyHeader{}
150+
},
151+
err: header.ErrEmptyRange,
152+
},
153+
{
154+
name: "zero trusted header",
155+
setup: func(suite *DummySuite) (*DummyHeader, []*DummyHeader) {
156+
var zero *DummyHeader
157+
return zero, []*DummyHeader{zero}
158+
},
159+
err: header.ErrZeroHeader,
160+
},
161+
{
162+
name: "zero untrusted header",
163+
setup: func(suite *DummySuite) (*DummyHeader, []*DummyHeader) {
164+
var zero *DummyHeader
165+
return zero, []*DummyHeader{zero}
166+
},
167+
err: header.ErrZeroHeader,
168+
},
169+
{
170+
name: "verification fails in middle of range",
171+
setup: func(suite *DummySuite) (*DummyHeader, []*DummyHeader) {
172+
trusted := suite.GenDummyHeaders(1)[0]
173+
headers := suite.GenDummyHeaders(5)
174+
headers[2].VerifyFailure = true // make 3rd header fail verification
175+
return trusted, headers
176+
},
177+
err: ErrDummyVerify,
178+
verified: 2,
179+
},
180+
{
181+
name: "non-adjacent header range ",
182+
setup: func(suite *DummySuite) (*DummyHeader, []*DummyHeader) {
183+
trusted := suite.GenDummyHeaders(1)[0]
184+
_ = suite.GenDummyHeaders(
185+
1,
186+
) // generate a header to ensure the range can be non-adjacent
187+
headers := suite.GenDummyHeaders(3)
188+
headers = append(headers[0:1], headers[2:]...)
189+
return trusted, headers
190+
},
191+
err: header.ErrNonAdjacentRange,
192+
verified: 1,
193+
},
194+
}
195+
196+
for _, tt := range tests {
197+
t.Run(tt.name, func(t *testing.T) {
198+
suite := NewTestSuite(t)
199+
trusted, untrstd := tt.setup(suite)
200+
verified, err := header.VerifyRange(trusted, untrstd)
201+
202+
if tt.err != nil {
203+
var verErr *header.VerifyError
204+
assert.ErrorAs(t, err, &verErr)
205+
assert.ErrorIs(t, errors.Unwrap(verErr), tt.err)
206+
assert.Len(t, verified, tt.verified)
207+
} else {
208+
assert.NoError(t, err)
209+
assert.Len(t, verified, len(untrstd))
210+
if len(untrstd) > 0 {
211+
assert.Equal(t,
212+
untrstd[len(untrstd)-1],
213+
verified[len(verified)-1],
214+
"last verified header should match last input header",
215+
)
216+
}
217+
}
218+
})
219+
}
220+
}

local/exchange.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ func (l *Exchange[H]) GetByHeight(ctx context.Context, height uint64) (H, error)
3434
return l.store.GetByHeight(ctx, height)
3535
}
3636

37-
func (l *Exchange[H]) GetRangeByHeight(ctx context.Context, from H, to uint64,
38-
) ([]H, error) {
39-
return l.store.GetRangeByHeight(ctx, from, to)
37+
func (l *Exchange[H]) GetRangeByHeight(ctx context.Context, from H, to uint64) ([]H, error) {
38+
headers, err := l.store.GetRangeByHeight(ctx, from, to)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
return header.VerifyRange(from, headers)
4044
}
4145

4246
func (l *Exchange[H]) Get(ctx context.Context, hash header.Hash) (H, error) {

p2p/session.go

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -281,41 +281,18 @@ func (s *session[H]) processResponses(responses []*p2p_pb.HeaderResponse) (h []H
281281
return nil, err
282282
}
283283

284-
return hdrs, s.verify(hdrs)
284+
return s.verify(hdrs)
285285
}
286286

287287
// verify checks that the received range of headers is adjacent and is valid against the provided
288288
// header.
289-
func (s *session[H]) verify(headers []H) error {
290-
// if `s.from` is empty, then additional validation for the header`s range is not needed.
289+
func (s *session[H]) verify(headers []H) ([]H, error) {
290+
// if `s.from` is empty, then we just trust the headers
291291
if s.from.IsZero() {
292-
return nil
292+
return headers, nil
293293
}
294294

295-
trusted := s.from
296-
// verify that the whole range is valid and adjacent.
297-
for _, untrusted := range headers {
298-
err := trusted.Verify(untrusted)
299-
if err != nil {
300-
return err
301-
}
302-
303-
// extra check for the adjacency should be performed only for the received range,
304-
// because headers are received out of order and `s.from` can't be adjacent to them
305-
if trusted.Height() != s.from.Height() {
306-
if trusted.Height()+1 != untrusted.Height() {
307-
// Exchange requires requested ranges to always consist of adjacent headers
308-
return fmt.Errorf(
309-
"peer sent valid but non-adjacent header. expected:%d, received:%d",
310-
trusted.Height()+1,
311-
untrusted.Height(),
312-
)
313-
}
314-
}
315-
// as `untrusted` was verified against previous trusted header, we can assume that it is valid
316-
trusted = untrusted
317-
}
318-
return nil
295+
return header.VerifyRange(s.from, headers)
319296
}
320297

321298
// prepareRequests converts incoming range into separate HeaderRequest.

p2p/session_test.go

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package p2p
22

33
import (
4-
"context"
54
"testing"
6-
"time"
75

8-
"github.com/libp2p/go-libp2p/core/peer"
9-
"github.com/stretchr/testify/assert"
106
"github.com/stretchr/testify/require"
11-
12-
"github.com/celestiaorg/go-header/headertest"
137
)
148

159
func Test_PrepareRequests(t *testing.T) {
@@ -20,39 +14,3 @@ func Test_PrepareRequests(t *testing.T) {
2014
require.Equal(t, requests[0].GetOrigin(), uint64(1))
2115
require.Equal(t, requests[1].GetOrigin(), uint64(6))
2216
}
23-
24-
// Test_Validate ensures that headers range is adjacent and valid.
25-
func Test_Validate(t *testing.T) {
26-
suite := headertest.NewTestSuite(t)
27-
head := suite.Head()
28-
ses := newSession(
29-
context.Background(),
30-
nil,
31-
&peerTracker{trackedPeers: make(map[peer.ID]*peerStat)},
32-
"", time.Second, nil,
33-
withValidation(head),
34-
)
35-
36-
headers := suite.GenDummyHeaders(5)
37-
err := ses.verify(headers)
38-
assert.NoError(t, err)
39-
}
40-
41-
// Test_ValidateFails ensures that non-adjacent range will return an error.
42-
func Test_ValidateFails(t *testing.T) {
43-
suite := headertest.NewTestSuite(t)
44-
head := suite.Head()
45-
ses := newSession(
46-
context.Background(),
47-
nil,
48-
&peerTracker{trackedPeers: make(map[peer.ID]*peerStat)},
49-
"", time.Second, nil,
50-
withValidation(head),
51-
)
52-
53-
headers := suite.GenDummyHeaders(5)
54-
// break adjacency
55-
headers[2] = headers[4]
56-
err := ses.verify(headers)
57-
assert.Error(t, err)
58-
}

store/store.go

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -304,67 +304,26 @@ func (s *Store[H]) Append(ctx context.Context, headers ...H) error {
304304
return nil
305305
}
306306

307-
var err error
308-
// take current write head to verify headers against
309-
var head H
310-
headPtr := s.writeHead.Load()
311-
if headPtr == nil {
312-
head, err = s.Head(ctx)
313-
if err != nil {
314-
return err
315-
}
316-
} else {
317-
head = *headPtr
318-
}
319-
320-
// collect valid headers
321-
verified := make([]H, 0, lh)
322-
for i, h := range headers {
323-
err = head.Verify(h)
324-
if err != nil {
325-
var verErr *header.VerifyError
326-
if errors.As(err, &verErr) {
327-
log.Errorw("invalid header",
328-
"height_of_head", head.Height(),
329-
"hash_of_head", head.Hash(),
330-
"height_of_invalid", h.Height(),
331-
"hash_of_invalid", h.Hash(),
332-
"reason", verErr.Reason)
333-
}
334-
// if the first header is invalid, no need to go further
335-
if i == 0 {
336-
// and simply return
337-
return err
338-
}
339-
// otherwise, stop the loop and apply headers appeared to be valid
340-
break
341-
}
342-
verified = append(verified, h)
343-
head = h
344-
}
345-
346307
onWrite := func() {
347-
newHead := verified[len(verified)-1]
308+
newHead := headers[len(headers)-1]
348309
s.writeHead.Store(&newHead)
349310
log.Infow("new head", "height", newHead.Height(), "hash", newHead.Hash())
350311
s.metrics.newHead(newHead.Height())
351312
}
352313

353314
// queue headers to be written on disk
354315
select {
355-
case s.writes <- verified:
356-
// we return an error here after writing,
357-
// as there might be an invalid header in between of a given range
316+
case s.writes <- headers:
358317
onWrite()
359-
return err
318+
return nil
360319
default:
361320
s.metrics.writesQueueBlocked(ctx)
362321
}
363322
// if the writes queue is full, we block until it is not
364323
select {
365-
case s.writes <- verified:
324+
case s.writes <- headers:
366325
onWrite()
367-
return err
326+
return nil
368327
case <-s.writesDn:
369328
return errStoppedStore
370329
case <-ctx.Done():

store/store_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,25 +126,6 @@ func TestStore_GetRangeByHeight_ExpectedRange(t *testing.T) {
126126
assert.Equal(t, lastHeaderInRangeHeight, out[len(out)-1].Height())
127127
}
128128

129-
func TestStore_Append_BadHeader(t *testing.T) {
130-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
131-
t.Cleanup(cancel)
132-
133-
suite := headertest.NewTestSuite(t)
134-
135-
ds := sync.MutexWrap(datastore.NewMapDatastore())
136-
store := NewTestStore(t, ctx, ds, suite.Head())
137-
138-
head, err := store.Head(ctx)
139-
require.NoError(t, err)
140-
assert.EqualValues(t, suite.Head().Hash(), head.Hash())
141-
142-
in := suite.GenDummyHeaders(10)
143-
in[0].VerifyFailure = true
144-
err = store.Append(ctx, in...)
145-
require.Error(t, err)
146-
}
147-
148129
// TestStore_GetRange tests possible combinations of requests and ensures that
149130
// the store can handle them adequately (even malformed requests)
150131
func TestStore_GetRange(t *testing.T) {

verify.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,45 @@ import (
66
"time"
77
)
88

9+
// VerifyRange verifies a range of adjacent untrusted Headers against trusted following general Header checks and
10+
// custom user-specific checks defined in [Verify].
11+
//
12+
// In case of error, returns verified sub-range of Headers and error.
13+
func VerifyRange[H Header[H]](trstd H, untrstdRange []H) ([]H, error) {
14+
if len(untrstdRange) == 0 {
15+
return nil, &VerifyError{Reason: ErrEmptyRange}
16+
}
17+
18+
verified := make([]H, 0, len(untrstdRange))
19+
for i, untrstd := range untrstdRange {
20+
err := Verify(trstd, untrstd)
21+
if err != nil {
22+
return verified, err
23+
}
24+
25+
// ensure range is adjacent, as Verify allows non-adjacency
26+
// NOTE: i > 0 because we allow the input trusted header to be non-adjacent
27+
// so given trusted header height 100, we can have untrusted 151-155
28+
if i > 0 && trstd.Height()+1 != untrstd.Height() {
29+
reason := fmt.Errorf(
30+
"%w; trusted: %d, expected_untrusted: %d, received_untrusted: %d",
31+
ErrNonAdjacentRange,
32+
trstd.Height(),
33+
trstd.Height()+1,
34+
untrstd.Height(),
35+
)
36+
// TODO(@Wondertan): Attach trusted and untrusted headers to the error
37+
// instead of manual error creation
38+
return verified, &VerifyError{Reason: reason}
39+
}
40+
41+
verified = append(verified, untrstd)
42+
trstd = untrstd
43+
}
44+
45+
return verified, nil
46+
}
47+
948
// Verify verifies untrusted Header against trusted following general Header checks and
1049
// custom user-specific checks defined in Header.Verify.
1150
//
@@ -87,11 +126,13 @@ func verify[H Header[H]](trstd, untrstd H) error {
87126
}
88127

89128
var (
90-
ErrZeroHeader = errors.New("zero header")
91-
ErrWrongChainID = errors.New("wrong chain id")
92-
ErrUnorderedTime = errors.New("unordered headers")
93-
ErrFromFuture = errors.New("header is from the future")
94-
ErrKnownHeader = errors.New("known header")
129+
ErrZeroHeader = errors.New("zero header")
130+
ErrWrongChainID = errors.New("wrong chain id")
131+
ErrUnorderedTime = errors.New("unordered headers")
132+
ErrFromFuture = errors.New("header is from the future")
133+
ErrKnownHeader = errors.New("known header")
134+
ErrEmptyRange = errors.New("empty header range")
135+
ErrNonAdjacentRange = errors.New("non-adjacent headers range")
95136
)
96137

97138
// VerifyError is thrown if a Header failed verification.

0 commit comments

Comments
 (0)