Skip to content

Commit

Permalink
refactor: harden verification
Browse files Browse the repository at this point in the history
  • Loading branch information
Wondertan committed Jul 31, 2023
1 parent 8e9341d commit fc0605d
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 75 deletions.
20 changes: 0 additions & 20 deletions errors.go

This file was deleted.

16 changes: 1 addition & 15 deletions headertest/dummy_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (d *DummyHeader) IsZero() bool {
}

func (d *DummyHeader) ChainID() string {
return "private"
return d.Raw.ChainID
}

func (d *DummyHeader) Hash() header.Hash {
Expand Down Expand Up @@ -108,20 +108,6 @@ func (d *DummyHeader) Verify(header header.Header) error {
if dummy, _ := header.(*DummyHeader); dummy.VerifyFailure {
return fmt.Errorf("header at height %d failed verification", header.Height())
}

epsilon := 10 * time.Second
if header.Time().After(time.Now().Add(epsilon)) {
return fmt.Errorf("header Time too far in the future")
}

if header.Height() <= d.Height() {
return fmt.Errorf("expected new header Height to be larger than old header Time")
}

if header.Time().Before(d.Time()) {
return fmt.Errorf("expected new header Time to be after old header Time")
}

return nil
}

Expand Down
5 changes: 3 additions & 2 deletions p2p/subscriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/libp2p/go-libp2p/core/peer"

"github.com/celestiaorg/go-header"
"github.com/celestiaorg/go-header/sync/verify"
)

// Subscriber manages the lifecycle and relationship of header Module
Expand Down Expand Up @@ -77,13 +78,13 @@ func (p *Subscriber[H]) SetVerifier(val func(context.Context, H) error) error {
// additional unmarhalling
msg.ValidatorData = hdr

var verErr *header.VerifyError
var verErr *verify.VerifyError
err = val(ctx, hdr)
switch {
case err == nil:
return pubsub.ValidationAccept
case errors.As(err, &verErr):
if verErr.Uncertain {
if verErr.SoftFailure {
return pubsub.ValidationIgnore
}
fallthrough
Expand Down
3 changes: 2 additions & 1 deletion store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
logging "github.com/ipfs/go-log/v2"

"github.com/celestiaorg/go-header"
"github.com/celestiaorg/go-header/sync/verify"
)

var log = logging.Logger("header/store")
Expand Down Expand Up @@ -333,7 +334,7 @@ func (s *Store[H]) Append(ctx context.Context, headers ...H) error {

err = head.Verify(h)
if err != nil {
var verErr *header.VerifyError
var verErr *verify.VerifyError
if errors.As(err, &verErr) {
log.Errorw("invalid header",
"height_of_head", head.Height(),
Expand Down
64 changes: 30 additions & 34 deletions sync/sync_head.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/celestiaorg/go-header"
"github.com/celestiaorg/go-header/sync/verify"
)

// Head returns the Network Head.
Expand Down Expand Up @@ -40,8 +41,8 @@ func (s *Syncer[H]) Head(ctx context.Context) (H, error) {
defer s.getter.Unlock()
netHead, err := s.getter.Head(ctx)
if err != nil {
log.Warnw("failed to return head from trusted peer, returning subjective head which may not be recent", "sbjHead", sbjHead.Height(), "err", err)
return sbjHead, nil
log.Warnw("failed to get recent head, returning current subjective", "sbjHead", sbjHead.Height(), "err", err)
return s.subjectiveHead(ctx)
}
// process and validate netHead fetched from trusted peers
// NOTE: We could trust the netHead like we do during 'automatic subjective initialization'
Expand Down Expand Up @@ -134,54 +135,49 @@ func (s *Syncer[H]) setSubjectiveHead(ctx context.Context, netHead H) {

// incomingNetworkHead processes new potential network headers.
// If the header valid, sets as new subjective header.
func (s *Syncer[H]) incomingNetworkHead(ctx context.Context, netHead H) error {
func (s *Syncer[H]) incomingNetworkHead(ctx context.Context, head H) error {
// ensure there is no racing between network head candidates
s.incomingMu.Lock()
defer s.incomingMu.Unlock()
// first of all, check the validity of the netHead
err := s.validateHead(ctx, netHead)
if err != nil {

softFailure, err := s.verify(ctx, head)
if err != nil && !softFailure {
return err
}
// and set it if valid
s.setSubjectiveHead(ctx, netHead)
return nil

// TODO(@Wondertan):
// Implement setSyncTarget and use it for soft failures
s.setSubjectiveHead(ctx, head)
return err
}

// validateHead checks validity of the given header against the subjective head.
func (s *Syncer[H]) validateHead(ctx context.Context, new H) error {
// verify verifies given network head candidate.
func (s *Syncer[H]) verify(ctx context.Context, newHead H) (bool, error) {
sbjHead, err := s.subjectiveHead(ctx)
if err != nil {
log.Errorw("getting subjective head during validation", "err", err)
// local error, so uncertain
return &header.VerifyError{Reason: err, Uncertain: true}
}
if new.Height() <= sbjHead.Height() {
log.Warnw("received known network header",
"current_height", sbjHead.Height(),
"header_height", new.Height(),
"header_hash", new.Hash())
// set uncertain, if it's from the past
return &header.VerifyError{Reason: err, Uncertain: true}
}
// perform verification
err = sbjHead.Verify(new)
var verErr *header.VerifyError
if errors.As(err, &verErr) {
// local error, so soft
return true, &verify.VerifyError{Reason: err, SoftFailure: true}
}

var heightThreshold int64
if s.Params.TrustingPeriod != 0 && s.Params.blockTime != 0 {
heightThreshold = int64(s.Params.TrustingPeriod / s.Params.blockTime)
}

var verErr *verify.VerifyError
err = verify.Verify(sbjHead, newHead, heightThreshold)
if errors.As(err, &verErr) && !verErr.SoftFailure {
log.Errorw("invalid network header",
"height_of_invalid", new.Height(),
"hash_of_invalid", new.Hash(),
"height_of_invalid", newHead.Height(),
"hash_of_invalid", newHead.Hash(),
"height_of_subjective", sbjHead.Height(),
"hash_of_subjective", sbjHead.Hash(),
"reason", verErr.Reason)
return verErr
}
// and accept if the header is good
return nil
}

// TODO(@Wondertan): We should request TrustingPeriod from the network's state params or
// listen for network params changes to always have a topical value.
return verErr.SoftFailure, err
}

// isExpired checks if header is expired against trusting period.
func isExpired(header header.Header, period time.Duration) bool {
Expand Down
7 changes: 4 additions & 3 deletions sync/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/celestiaorg/go-header/headertest"
"github.com/celestiaorg/go-header/local"
"github.com/celestiaorg/go-header/store"
"github.com/celestiaorg/go-header/sync/verify"
)

func TestSyncSimpleRequestingHead(t *testing.T) {
Expand Down Expand Up @@ -277,10 +278,9 @@ func TestSyncerIncomingDuplicate(t *testing.T) {

time.Sleep(time.Millisecond * 10)

var verErr *header.VerifyError
var verErr *verify.VerifyError
err = syncer.incomingNetworkHead(ctx, range1[len(range1)-1])
assert.ErrorAs(t, err, &verErr)
assert.True(t, verErr.Uncertain)

err = syncer.SyncWait(ctx)
require.NoError(t, err)
Expand Down Expand Up @@ -357,7 +357,8 @@ func TestSync_InvalidSyncTarget(t *testing.T) {
// a new sync job to a good sync target
expectedHead, err := remoteStore.Head(ctx)
require.NoError(t, err)
syncer.incomingNetworkHead(ctx, expectedHead)
err = syncer.incomingNetworkHead(ctx, expectedHead)
require.NoError(t, err)

// wait for syncer to finish (give it a bit of time to register
// new job with new sync target)
Expand Down
112 changes: 112 additions & 0 deletions sync/verify/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// TODO(@Wondertan): Should be just part of sync pkg and not subpkg
//
// Fix after adjacency requirement is removed from the Store.
package verify

import (
"errors"
"fmt"
"time"

"github.com/celestiaorg/go-header"
)

// DefaultHeightThreshold defines default height threshold beyond which headers are rejected
// NOTE: Compared against subjective head which is guaranteed to be non-expired
const DefaultHeightThreshold int64 = 40000 // ~ 7 days of 15 second headers

// VerifyError is thrown during for Headers failed verification.
type VerifyError struct {
// Reason why verification failed as inner error.
Reason error
// SoftFailure means verification did not have enough information to definitively conclude a
// Header was correct or not.
// May happen with recent Headers during unfinished historical sync or because of local errors.
// TODO(@Wondertan): Better be part of signature Header.Verify() (bool, error), but kept here
// not to break
SoftFailure bool
}

func (vr *VerifyError) Error() string {
return fmt.Sprintf("header: verify: %s", vr.Reason.Error())
}

func (vr *VerifyError) Unwrap() error {
return vr.Reason
}

// Verify verifies untrusted Header against trusted following general Header checks and
// custom user-specific checks defined in Header.Verify
//
// If heightThreshold is zero, uses DefaultHeightThreshold.
// Always returns VerifyError.
func Verify[H header.Header](trstd, untrstd H, heightThreshold int64) error {
// general mandatory verification
err := verify[H](trstd, untrstd, heightThreshold)
if err != nil {
return &VerifyError{Reason: err}
}
// user defined verification
err = trstd.Verify(untrstd)
if err == nil {
return nil
}
// if that's an error, ensure we always return VerifyError
var verErr *VerifyError
if !errors.As(err, &verErr) {
verErr = &VerifyError{Reason: err}
}
// check adjacency of failed verification
adjacent := untrstd.Height() == trstd.Height()+1
if !adjacent {
// if non-adjacent, we don't know if the header is *really* wrong
// so set as soft
verErr.SoftFailure = true
}
// we trust adjacent verification to it's fullest
// if verification fails - the header is *really* wrong
return verErr
}

// verify is a little bro of Verify yet performs mandatory Header checks
// for any Header implementation.
func verify[H header.Header](trstd, untrstd H, heightThreshold int64) error {
if heightThreshold == 0 {
heightThreshold = DefaultHeightThreshold
}

if untrstd.IsZero() {
return fmt.Errorf("zero header")
}

if untrstd.ChainID() != trstd.ChainID() {
return fmt.Errorf("wrong header chain id %s, not %s", untrstd.ChainID(), trstd.ChainID())
}

if !untrstd.Time().After(trstd.Time()) {
return fmt.Errorf("unordered header timestamp %v is before %v", untrstd.Time(), trstd.Time())
}

now := time.Now()
if !untrstd.Time().Before(now.Add(clockDrift)) {
return fmt.Errorf("header timestamp %v is from future (now: %v, clock_drift: %v)", untrstd.Time(), now, clockDrift)
}

known := untrstd.Height() <= trstd.Height()
if known {
return fmt.Errorf("known header height %d, current %d", untrstd.Height(), trstd.Height())
}
// reject headers with height too far from the future
// this is essential for headers failed non-adjacent verification
// yet taken as sync target
adequateHeight := untrstd.Height()-trstd.Height() < heightThreshold
if !adequateHeight {
return fmt.Errorf("header height %d is far from future (current: %d, threshold: %d)", untrstd.Height(), trstd.Height(), heightThreshold)
}

return nil
}

// clockDrift defines how much new header's time can drift into
// the future relative to the now time during verification.
var clockDrift = 10 * time.Second
Loading

0 comments on commit fc0605d

Please sign in to comment.