diff --git a/types/header.go b/types/header.go new file mode 100644 index 0000000000..3f339ca30e --- /dev/null +++ b/types/header.go @@ -0,0 +1,239 @@ +package types + +import ( + "bytes" + "encoding" + "encoding/hex" + "fmt" + "strings" + "time" +) + +// TODO: remove Hash and H definition after importing go-header package +type Hash []byte + +func (h Hash) String() string { + return strings.ToUpper(hex.EncodeToString(h)) +} + +func (h Hash) MarshalJSON() ([]byte, error) { + s := strings.ToUpper(hex.EncodeToString(h)) + jbz := make([]byte, len(s)+2) + jbz[0] = '"' + copy(jbz[1:], s) + jbz[len(jbz)-1] = '"' + return jbz, nil +} + +func (h *Hash) UnmarshalJSON(data []byte) error { + if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' { + return fmt.Errorf("invalid hex string: %s", data) + } + bz2, err := hex.DecodeString(string(data[1 : len(data)-1])) + if err != nil { + return err + } + *h = bz2 + return nil +} + +// Header abstracts all methods required to perform header sync. +type H interface { + // New creates new instance of a header. + New() H + // Hash returns hash of a header. + Hash() [32]byte + // Number returns the height of a header. + Number() int64 + // LastHeader returns the hash of last header before this header (aka. previous header hash). + LastHeader() [32]byte + // Timestamp returns time when header was created. + Timestamp() time.Time + // IsRecent checks if header is recent against the given blockTime. + IsRecent(duration time.Duration) bool + // IsExpired checks if header is expired against trusting period. + IsExpired() bool + // VerifyAdjacent validates adjacent untrusted header against trusted header. + VerifyAdjacent(H) error + // VerifyNonAdjacent validates non-adjacent untrusted header against trusted header. + VerifyNonAdjacent(H) error + // Verify performs basic verification of untrusted header. + Verify(H) error + // Validate performs basic validation to check for missed/incorrect fields. + Validate() error + + encoding.BinaryMarshaler + encoding.BinaryUnmarshaler +} + +var _ H = (*Header)(nil) + +// New creates new instance of a header. +func (h *Header) New() H { + return &Header{} +} + +// Number returns the height of a header. +func (h *Header) Number() int64 { + return int64(h.Height) +} + +// Timestamp returns time when header was created. +func (h *Header) Timestamp() time.Time { + return time.Unix(int64(h.Time), 0) +} + +// LastHeader returns the hash of last header before this header (aka. previous header hash). +func (h *Header) LastHeader() [32]byte { + return h.LastHeaderHash +} + +// IsRecent checks if header is recent against the given blockTime. +func (h *Header) IsRecent(blockTime time.Duration) bool { + return time.Since(time.Unix(int64(h.Time), 0)) <= blockTime +} + +// TODO(@Wondertan): We should request TrustingPeriod from the network's state params or +// listen for network params changes to always have a topical value. + +// TrustingPeriod is period through which we can trust a header's validators set. +// +// Should be significantly less than the unbonding period (e.g. unbonding +// period = 3 weeks, trusting period = 2 weeks). +// +// More specifically, trusting period + time needed to check headers + time +// needed to report and punish misbehavior should be less than the unbonding +// period. +var TrustingPeriod = 168 * time.Hour + +// IsExpired checks if header is expired against trusting period. +func (h *Header) IsExpired() bool { + expirationTime := time.Unix(int64(h.Time), 0).Add(TrustingPeriod) + return !expirationTime.After(time.Now()) +} + +// VerifyError is thrown on during VerifyAdjacent and VerifyNonAdjacent if verification fails. +type VerifyError struct { + Reason error +} + +func (vr *VerifyError) Error() string { + return fmt.Sprintf("header: verify: %s", vr.Reason.Error()) +} + +// VerifyAdjacent validates adjacent untrusted header against trusted header. +func (h *Header) VerifyAdjacent(untrst H) error { + untrstH, ok := untrst.(*Header) + if !ok { + return &VerifyError{ + fmt.Errorf("%T is not of type %T", untrst, h), + } + } + + if untrstH.Height != h.Height+1 { + return &VerifyError{ + fmt.Errorf("headers must be adjacent in height: trusted %d, untrusted %d", h.Height, untrstH.Height), + } + } + + if err := verifyNewHeaderAndVals(h, untrstH); err != nil { + return &VerifyError{Reason: err} + } + + // Check the validator hashes are the same + // TODO: next validator set is not available + if !bytes.Equal(untrstH.AggregatorsHash[:], h.AggregatorsHash[:]) { + return &VerifyError{ + fmt.Errorf("expected old header next validators (%X) to match those from new header (%X)", + h.AggregatorsHash, + untrstH.AggregatorsHash, + ), + } + } + + return nil +} + +// VerifyNonAdjacent validates non-adjacent untrusted header against trusted header. +func (h *Header) VerifyNonAdjacent(untrst H) error { + untrstH, ok := untrst.(*Header) + if !ok { + return &VerifyError{ + fmt.Errorf("%T is not of type %T", untrst, h), + } + } + if untrstH.Height == h.Height+1 { + return &VerifyError{ + fmt.Errorf( + "headers must be non adjacent in height: trusted %d, untrusted %d", + h.Height, + untrstH.Height, + ), + } + } + + if err := verifyNewHeaderAndVals(h, untrstH); err != nil { + return &VerifyError{Reason: err} + } + + // Ensure that untrusted commit has enough of trusted commit's power. + // err := h.ValidatorSet.VerifyCommitLightTrusting(eh.ChainID, untrst.Commit, light.DefaultTrustLevel) + // if err != nil { + // return &VerifyError{err} + // } + + return nil +} + +// clockDrift defines how much new header's time can drift into +// the future relative to the now time during verification. +var maxClockDrift = 10 * time.Second + +// verifyNewHeaderAndVals performs basic verification of untrusted header. +func verifyNewHeaderAndVals(trusted, untrusted *Header) error { + if err := untrusted.ValidateBasic(); err != nil { + return fmt.Errorf("untrusted.ValidateBasic failed: %w", err) + } + + if untrusted.Height <= trusted.Height { + return fmt.Errorf("expected new header height %d to be greater than one of old header %d", + untrusted.Height, + trusted.Height) + } + + if !untrusted.Timestamp().After(trusted.Timestamp()) { + return fmt.Errorf("expected new header time %v to be after old header time %v", + untrusted.Time, + trusted.Time) + } + + if !untrusted.Timestamp().Before(time.Now().Add(maxClockDrift)) { + return fmt.Errorf("new header has a time from the future %v (now: %v; max clock drift: %v)", + untrusted.Time, + time.Now(), + maxClockDrift) + } + + return nil +} + +// Verify combines both VerifyAdjacent and VerifyNonAdjacent functions. +func (h *Header) Verify(untrst H) error { + untrstH, ok := untrst.(*Header) + if !ok { + return &VerifyError{ + fmt.Errorf("%T is not of type %T", untrst, h), + } + } + + if untrstH.Height != h.Height+1 { + return h.VerifyNonAdjacent(untrst) + } + + return h.VerifyAdjacent(untrst) +} + +// Validate performs basic validation to check for missed/incorrect fields. +func (h *Header) Validate() error { + return h.ValidateBasic() +} diff --git a/types/header_test.go b/types/header_test.go new file mode 100644 index 0000000000..398c42c1e3 --- /dev/null +++ b/types/header_test.go @@ -0,0 +1,196 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type A struct { +} + +func (a *A) New() H { + return &A{} +} + +func (a *A) Number() int64 { + return 0 +} + +func (a *A) Timestamp() time.Time { + return time.Now() +} + +func (a *A) Hash() [32]byte { + return [32]byte{} +} + +func (a *A) IsExpired() bool { + return false +} + +func (a *A) IsRecent(duration time.Duration) bool { + return true +} + +func (a *A) LastHeader() [32]byte { + return [32]byte{} +} + +func (a *A) Verify(h H) error { + return nil +} + +func (a *A) VerifyAdjacent(h H) error { + return nil +} + +func (a *A) VerifyNonAdjacent(h H) error { + return nil +} + +func (a *A) Validate() error { + return nil +} + +func (a *A) MarshalBinary() ([]byte, error) { + return []byte{}, nil +} + +func (a *A) UnmarshalBinary(data []byte) error { + return nil +} + +func TestInterfaceCompatible(t *testing.T) { + assert := assert.New(t) + h := &Header{} + var a H = &A{} + err := h.VerifyAdjacent(a) + assert.Error(err) +} + +func TestNew(t *testing.T) { + assert := assert.New(t) + h1 := &Header{Time: 123456} + + h2 := h1.New() + assert.NotNil(h2) +} + +func TestHash(t *testing.T) { + assert := assert.New(t) + h1 := &Header{Height: 123456} + + h := [32]byte{104, 232, 90, 84, 228, 141, 242, 116, 213, 125, 19, 72, 23, 49, 5, 255, 5, 82, 174, 209, 213, 171, 106, 106, 156, 227, 119, 225, 24, 130, 129, 185} + + assert.Equal(h, h1.Hash()) +} + +func TestNumber(t *testing.T) { + assert := assert.New(t) + h1 := &Header{Height: 123456} + + assert.Equal(h1.Number(), int64(123456)) +} + +func TestLastHeader(t *testing.T) { + assert := assert.New(t) + h := [32]byte{104, 232, 90, 84, 228, 141, 242, 116, 213, 125, 19, 72, 23, 49, 5, 255, 5, 82, 174, 209, 213, 171, 106, 106, 156, 227, 119, 225, 24, 130, 129, 185} + h1 := &Header{LastHeaderHash: h} + + assert.Equal(h1.LastHeader(), h) +} + +func TestTimestamp(t *testing.T) { + assert := assert.New(t) + h1 := &Header{Time: 123456} + + t1 := h1.Timestamp() + assert.Equal(t1.Unix(), int64(123456)) +} + +func TestIsRecent(t *testing.T) { + assert := assert.New(t) + h1 := &Header{Time: 123456} + t2 := time.Now() + h2 := &Header{Time: uint64(t2.Unix())} + + recent := h1.IsRecent(time.Hour) + assert.False(recent) + + recent = h2.IsRecent(time.Hour) + assert.True(recent) +} + +func TestIsExpired(t *testing.T) { + assert := assert.New(t) + h1 := &Header{Time: 123456} + t2 := time.Now() + h2 := &Header{Time: uint64(t2.Unix())} + + expired := h1.IsExpired() + assert.True(expired) + + expired = h2.IsExpired() + assert.False(expired) +} + +var case_adj = []struct { + name string + input *Header +}{ + {"trusted", &Header{ProposerAddress: []byte("123"), Time: 123, Height: 1}}, + {"untrusted", &Header{ProposerAddress: []byte("123"), Time: 456, Height: 2}}, +} + +var case_non_adj = []struct { + name string + input *Header +}{ + {"trusted", &Header{ProposerAddress: []byte("123"), Time: 123, Height: 1}}, + {"untrusted", &Header{ProposerAddress: []byte("123"), Time: 456, Height: 3}}, +} + +func TestVerifyAdjacent(t *testing.T) { + assert := assert.New(t) + + err := case_adj[0].input.VerifyAdjacent(case_adj[1].input) + assert.NoError(err) + + err = case_non_adj[0].input.VerifyAdjacent(case_non_adj[1].input) + assert.Error(err) +} + +func TestVerifyNonAdjacent(t *testing.T) { + assert := assert.New(t) + + err := case_non_adj[0].input.VerifyNonAdjacent(case_non_adj[1].input) + assert.NoError(err) + + err = case_adj[0].input.VerifyNonAdjacent(case_adj[1].input) + assert.Error(err) +} + +func TestVerify(t *testing.T) { + assert := assert.New(t) + + err := case_adj[0].input.Verify(case_adj[1].input) + assert.NoError(err) + + err = case_non_adj[0].input.Verify(case_non_adj[1].input) + assert.NoError(err) +} + +func TestValidate(t *testing.T) { + assert := assert.New(t) + h := &Header{} + err := h.Validate() + assert.Error(err) + + h = &Header{ + ProposerAddress: []byte("123"), + } + err = h.Validate() + assert.NoError(err) +}