Search Apps Documentation Source Content File Folder Download Copy Actions Download

height.gno

6.17 Kb · 190 lines
  1package types
  2
  3import (
  4	"chain/runtime"
  5	"encoding/binary"
  6	"regexp"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/aib/encoding/proto"
 11	"gno.land/p/nt/ufmt/v0"
 12)
 13
 14// Height is a monotonically increasing data type
 15// that can be compared against another Height for the purposes of updating and
 16// freezing clients
 17//
 18// Normally the RevisionHeight is incremented at each height while keeping
 19// RevisionNumber the same. However some consensus algorithms may choose to
 20// reset the height in certain conditions e.g. hard forks, state-machine
 21// breaking changes In these cases, the RevisionNumber is incremented so that
 22// height continues to be monitonically increasing even as the RevisionHeight
 23// gets reset
 24//
 25// Please note that json tags for generated Go code are overridden to explicitly exclude the omitempty jsontag.
 26// This enforces the Go json marshaller to always emit zero values for both revision_number and revision_height.
 27type Height struct {
 28	// the revision that the client is currently on
 29	RevisionNumber uint64
 30	// the height within the given revision
 31	RevisionHeight uint64
 32}
 33
 34func NewHeight(revision, height uint64) Height {
 35	return Height{
 36		RevisionNumber: revision,
 37		RevisionHeight: height,
 38	}
 39}
 40
 41// ParseHeight is a utility function that takes a string representation of the
 42// height and returns a Height struct.
 43func ParseHeight(heightStr string) (Height, error) {
 44	splitStr := strings.Split(heightStr, "/")
 45	if len(splitStr) != 2 {
 46		return Height{}, ufmt.Errorf("expected height string format: {revision}/{height}. Got: %s", heightStr)
 47	}
 48	revisionNumber, err := strconv.ParseUint(splitStr[0], 10, 64)
 49	if err != nil {
 50		return Height{}, ufmt.Errorf("invalid revision number. parse err: %s", err)
 51	}
 52	revisionHeight, err := strconv.ParseUint(splitStr[1], 10, 64)
 53	if err != nil {
 54		return Height{}, ufmt.Errorf("invalid revision height. parse err: %s", err)
 55	}
 56	return NewHeight(revisionNumber, revisionHeight), nil
 57}
 58
 59func ZeroHeight() Height { return Height{} }
 60
 61func (h Height) String() string {
 62	return ufmt.Sprintf("%d/%d", h.RevisionNumber, h.RevisionHeight)
 63}
 64
 65// StringNatSort returns a natural sortable representation of height that can
 66// be used for store keys.
 67func (h Height) StringNatSort() string {
 68	bz := make([]byte, 16)
 69	binary.BigEndian.PutUint64(bz, h.RevisionNumber)
 70	binary.BigEndian.PutUint64(bz[8:], h.RevisionHeight)
 71	return string(bz)
 72}
 73
 74// ParseHeightNatSort is a utility function that takes a string nat sorted
 75// representation of the height (see StringBatSort) and returns a Height
 76// struct.
 77func ParseHeightNatSort(heightNatSort string) (h Height) {
 78	bz := []byte(heightNatSort)
 79	h.RevisionNumber = binary.BigEndian.Uint64(bz)
 80	h.RevisionHeight = binary.BigEndian.Uint64(bz[8:])
 81	return
 82}
 83
 84// GTE Helper comparison function returns true if h >= other
 85func (h Height) GTE(other Height) bool {
 86	cmp := h.Compare(other)
 87	return cmp >= 0
 88}
 89
 90// GT Helper comparison function returns true if h > other
 91func (h Height) GT(other Height) bool {
 92	return h.Compare(other) == 1
 93}
 94
 95// LTE Helper comparison function returns true if h <= other
 96func (h Height) LTE(other Height) bool {
 97	cmp := h.Compare(other)
 98	return cmp <= 0
 99}
100
101// LT Helper comparison function returns true if h < other
102func (h Height) LT(other Height) bool {
103	return h.Compare(other) == -1
104}
105
106// EQ Helper comparison function returns true if h == other
107func (h Height) EQ(other Height) bool {
108	return h.Compare(other) == 0
109}
110
111// IsZero returns true if height revision and revision-height are both 0
112func (h Height) IsZero() bool {
113	return h.RevisionNumber == 0 && h.RevisionHeight == 0
114}
115
116// Compare implements a method to compare two heights. When comparing two heights a, b
117// we can call a.Compare(b) which will return
118// -1 if a < b
119// 0  if a = b
120// 1  if a > b
121//
122// It first compares based on revision numbers, whichever has the higher revision number is the higher height
123// If revision number is the same, then the revision height is compared
124func (h Height) Compare(height Height) int64 {
125	if h.RevisionNumber < height.RevisionNumber {
126		return -1
127	}
128	if h.RevisionNumber > height.RevisionNumber {
129		return 1
130	}
131	if h.RevisionHeight < height.RevisionHeight {
132		return -1
133	}
134	if h.RevisionHeight > height.RevisionHeight {
135		return 1
136	}
137	return 0
138}
139
140// GetSelfHeight is a utility function that returns self height given context.
141// Revision number is retrieved from runtime.ChainID()
142func GetSelfHeight() Height {
143	revision := ParseChainID(runtime.ChainID())
144	return NewHeight(revision, uint64(runtime.ChainHeight()))
145}
146
147// IsRevisionFormat checks if a chainID is in the format required for parsing
148// revisions
149// The chainID must be in the form: `{chainID}-{revision}`.
150// 24-host may enforce stricter checks on chainID
151var IsRevisionFormat = regexp.MustCompile(`^.*[^\n-]-{1}[1-9][0-9]*$`).MatchString
152
153// ParseChainID is a utility function that returns a revision number from the
154// given ChainID.
155// ParseChainID attempts to parse a chain id in the format:
156// `{chainID}-{revision}` and return the revisionnumber as a uint64.
157// If the chainID is not in the expected format, a default revision value of 0
158// is returned.
159func ParseChainID(chainID string) uint64 {
160	if !IsRevisionFormat(chainID) {
161		// chainID is not in revision format, return 0 as default
162		return 0
163	}
164	splitStr := strings.Split(chainID, "-")
165	revision, err := strconv.ParseUint(splitStr[len(splitStr)-1], 10, 64)
166	// sanity check: error should always be nil since regex only allows numbers in last element
167	if err != nil {
168		panic(ufmt.Errorf("regex allowed non-number value as last split element for chainID: %s", chainID))
169	}
170	return revision
171}
172
173// SetRevisionNumber takes a chainID in valid revision format and swaps the
174// revision number in the chainID with the given revision number.
175func SetRevisionNumber(chainID string, revision uint64) (string, error) {
176	if !IsRevisionFormat(chainID) {
177		return "", ufmt.Errorf("chainID is not in revision format: %s", chainID)
178	}
179
180	splitStr := strings.Split(chainID, "-")
181	// swap out revision number with given revision
182	splitStr[len(splitStr)-1] = strconv.FormatUint(revision, 10)
183	return strings.Join(splitStr, "-"), nil
184}
185
186func (h Height) ProtoMarshal() (bz []byte) {
187	bz = proto.AppendVarint(bz, 1, h.RevisionNumber)
188	bz = proto.AppendVarint(bz, 2, h.RevisionHeight)
189	return
190}