block_store.gno
8.39 Kb · 332 lines
1package block
2
3import "gno.land/p/akkadia/v0/ds/btree"
4
5const defaultSystemBlockLimit uint32 = 100000
6
7var blockStore = newBlockStore()
8
9// BlockStore owns block records and secondary indexes.
10//
11// Data shape:
12// - records: blockID(uint32) -> query-string payload(string)
13// - nameIndex: name(string) -> blockID(uint32)
14// - metadatas: blockID(uint32) -> metadata(string)
15// - acrRewardAllowlist: blockID(uint32) -> bool
16//
17// Responsibilities:
18// - allocate normal block IDs and store canonical block records by block ID
19// - keep block name indexes and ACR reward allowlists in sync with record mutations
20// - protect store-owned invariants such as duplicate block IDs and names
21// - return defensive copies from public read methods
22//
23// Non-responsibilities:
24// - validate external parameter shape such as block name format, supply range, or pagination
25// - parse user-facing encoded input such as CSV property payloads
26// - perform auth, freeze, migration, payment, or event handling
27// - interpret economics beyond store-owned existence and index consistency
28type BlockStore struct {
29 records *btree.Uint32BTree
30 nameIndex *btree.StringBTree
31 metadatas *btree.Uint32BTree
32 acrRewardAllowlist *btree.Uint32BTree
33 systemBlockLimit uint32
34 maxSystemBlockID uint32
35 nextBlockID uint32
36}
37
38func newBlockStore() *BlockStore {
39 return &BlockStore{
40 records: btree.NewUint32BTree(32),
41 nameIndex: btree.NewStringBTree(32),
42 metadatas: btree.NewUint32BTree(32),
43 acrRewardAllowlist: btree.NewUint32BTree(32),
44 systemBlockLimit: defaultSystemBlockLimit,
45 maxSystemBlockID: 0,
46 nextBlockID: defaultSystemBlockLimit,
47 }
48}
49
50// ==================== DANGER: MUTABLE MIGRATION CAPABILITIES ====================
51//
52// The getters in this section expose mutable store internals.
53// Only authorized migration exporter paths may depend on these capabilities.
54// Runtime reads and test setup must use copy-returning methods or total helpers.
55func (s *BlockStore) Records() *btree.Uint32BTree {
56 return s.records
57}
58
59func (s *BlockStore) NameIndex() *btree.StringBTree {
60 return s.nameIndex
61}
62
63func (s *BlockStore) AcrRewardAllowlist() *btree.Uint32BTree {
64 return s.acrRewardAllowlist
65}
66
67func (s *BlockStore) Metadatas() *btree.Uint32BTree {
68 return s.metadatas
69}
70
71// ================= END DANGER: MUTABLE MIGRATION CAPABILITIES ==================
72
73func (s *BlockStore) Create(record map[string]string) uint32 {
74 name := record["name"]
75 if s.nameIndex.Has(name) {
76 panic("name already exists: " + name)
77 }
78
79 blockID := s.nextID()
80 s.set(blockID, record)
81 return blockID
82}
83
84func (s *BlockStore) CreateSystem(blockID uint32, record map[string]string) {
85 name := record["name"]
86 blockIDStr := blockIDToString(blockID)
87 if !s.IsSystemID(blockID) {
88 panic("block ID must be less than 100000")
89 }
90 if _, found := s.records.Get(blockID); found {
91 panic("block already exists: " + blockIDStr)
92 }
93 if s.nameIndex.Has(name) {
94 panic("name already exists: " + name)
95 }
96
97 if s.maxSystemBlockID < blockID {
98 s.maxSystemBlockID = blockID
99 }
100
101 s.set(blockID, record)
102}
103
104func (s *BlockStore) Delete(blockID uint32) {
105 block := s.MustGet(blockID)
106 s.nameIndex.Remove(block["name"])
107 s.records.Remove(blockID)
108 s.metadatas.Remove(blockID)
109 s.acrRewardAllowlist.Remove(blockID)
110}
111
112func (s *BlockStore) Update(blockID uint32, updates map[string]string) map[string]string {
113 current := s.mustGetDecoded(blockID)
114 oldName := current["name"]
115 newName := oldName
116 if name, found := updates["name"]; found {
117 newName = name
118 }
119 if oldName != newName {
120 if s.nameIndex.Has(newName) {
121 panic("name already exists: " + newName)
122 }
123 }
124
125 updated := copyStringMap(current)
126 for key, val := range updates {
127 updated[key] = val
128 }
129 updated["id"] = blockIDToString(blockID)
130
131 if oldName != newName {
132 s.nameIndex.Remove(oldName)
133 s.nameIndex.Set(newName, blockID)
134 }
135
136 s.records.Set(blockID, encodeStringMap(updated))
137 return copyStringMap(updated)
138}
139
140func (s *BlockStore) Has(blockID uint32) bool {
141 return s.records.Has(blockID)
142}
143
144func (s *BlockStore) Get(blockID uint32) (map[string]string, bool) {
145 blockVal, found := s.records.Get(blockID)
146 if !found {
147 return nil, false
148 }
149 return decodeStringMap(blockVal.(string)), true
150}
151
152func (s *BlockStore) MustGet(blockID uint32) map[string]string {
153 return s.mustGetDecoded(blockID)
154}
155
156func (s *BlockStore) mustGetDecoded(blockID uint32) map[string]string {
157 blockVal, found := s.records.Get(blockID)
158 if !found {
159 panic("block not found: " + blockIDToString(blockID))
160 }
161 return decodeStringMap(blockVal.(string))
162}
163
164func (s *BlockStore) mustHave(blockID uint32) {
165 if !s.records.Has(blockID) {
166 panic("block not found: " + blockIDToString(blockID))
167 }
168}
169
170func (s *BlockStore) MustGetByName(name string) map[string]string {
171 blockIDVal, found := s.nameIndex.Get(name)
172 if !found {
173 panic("block not found by name: " + name)
174 }
175 return s.MustGet(blockIDVal.(uint32))
176}
177
178func (s *BlockStore) List(page, count int) []map[string]string {
179 result := []map[string]string{}
180 offset := (page - 1) * count
181 s.records.IterateByOffset(offset, count, func(_ uint32, value interface{}) bool {
182 result = append(result, decodeStringMap(value.(string)))
183 return false
184 })
185 return result
186}
187
188func (s *BlockStore) ListFromTo(from, to *uint32, limit uint32) []map[string]string {
189 result := []map[string]string{}
190 if limit == 0 {
191 return result
192 }
193
194 if from != nil && to != nil && *from > *to {
195 panic("from is greater than to")
196 }
197
198 var end *uint32
199 if to != nil && *to < ^uint32(0) {
200 endValue := *to + 1
201 end = &endValue
202 }
203 s.records.Iterate(from, end, func(_ uint32, value any) bool {
204 result = append(result, decodeStringMap(value.(string)))
205 return len(result) >= int(limit)
206 })
207 return result
208}
209
210func (s *BlockStore) ListByIDs(blockIDs ...uint32) []map[string]string {
211 result := []map[string]string{}
212 for _, blockID := range blockIDs {
213 block, found := s.Get(blockID)
214 if found {
215 result = append(result, block)
216 }
217 }
218 return result
219}
220
221func (s *BlockStore) SetMetadata(blockID uint32, metadata string) {
222 s.mustHave(blockID)
223 s.metadatas.Set(blockID, metadata)
224}
225
226func (s *BlockStore) GetBlockMetadata(blockID uint32) string {
227 s.mustHave(blockID)
228 metadata, found := s.metadatas.Get(blockID)
229 if !found {
230 return ""
231 }
232 return metadata.(string)
233}
234
235func (s *BlockStore) ListBlockMetadataByIDs(blockIDs ...uint32) []string {
236 result := []string{}
237 for _, blockID := range blockIDs {
238 if !s.records.Has(blockID) {
239 continue
240 }
241 metadata, found := s.metadatas.Get(blockID)
242 if !found {
243 result = append(result, "")
244 continue
245 }
246 result = append(result, metadata.(string))
247 }
248 return result
249}
250
251func (s *BlockStore) Total() int {
252 return s.records.Size()
253}
254
255func (s *BlockStore) NameIndexTotal() int {
256 return s.nameIndex.Size()
257}
258
259func (s *BlockStore) SetAcrRewardEnabled(blockID uint32) {
260 if !s.Has(blockID) {
261 panic("block not found: " + blockIDToString(blockID))
262 }
263 s.acrRewardAllowlist.Set(blockID, true)
264}
265
266func (s *BlockStore) UnsetAcrRewardEnabled(blockID uint32) {
267 blockIDStr := blockIDToString(blockID)
268 if !s.acrRewardAllowlist.Has(blockID) {
269 panic("block not in acr reward allowlist: " + blockIDStr)
270 }
271 s.acrRewardAllowlist.Remove(blockID)
272}
273
274func (s *BlockStore) IsAcrRewardEnabled(blockID uint32) bool {
275 return s.acrRewardAllowlist.Has(blockID)
276}
277
278func (s *BlockStore) ListAcrRewardEnabled() []string {
279 result := []string{}
280 s.acrRewardAllowlist.Iterate(nil, nil, func(key uint32, value any) bool {
281 result = append(result, blockIDToString(key))
282 return false
283 })
284 return result
285}
286
287func (s *BlockStore) AcrRewardTotal() int {
288 return s.acrRewardAllowlist.Size()
289}
290
291func (s *BlockStore) SystemTotal() int {
292 result := 0
293 for i := uint32(0); i <= s.maxSystemBlockID; i++ {
294 if s.records.Has(i) {
295 result++
296 }
297 }
298 return result
299}
300
301func (s *BlockStore) NormalTotal() int {
302 return s.Total() - s.SystemTotal()
303}
304
305func (s *BlockStore) NextID() uint32 {
306 return s.nextBlockID
307}
308
309func (s *BlockStore) SystemBlockLimit() uint32 {
310 return s.systemBlockLimit
311}
312
313func (s *BlockStore) MaxSystemBlockID() uint32 {
314 return s.maxSystemBlockID
315}
316
317func (s *BlockStore) IsSystemID(blockID uint32) bool {
318 return blockID < s.systemBlockLimit
319}
320
321func (s *BlockStore) nextID() uint32 {
322 id := s.nextBlockID
323 s.nextBlockID++
324 return id
325}
326
327func (s *BlockStore) set(blockID uint32, record map[string]string) {
328 stored := copyStringMap(record)
329 stored["id"] = blockIDToString(blockID)
330 s.records.Set(blockID, encodeStringMap(stored))
331 s.nameIndex.Set(stored["name"], blockID)
332}