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}