kv_store.gno
7.28 Kb · 287 lines
1package store
2
3import (
4 bptree "gno.land/p/nt/bptree/v0"
5)
6
7// kvStore represents a domain-specific key-value storage
8// Each domain (pool, position, etc.) creates its own kvStore instance
9type kvStore struct {
10 data map[string]any // key -> value
11 authorizedCallers map[address]Permission
12 domainAddress address
13}
14
15// NewKVStore creates a new kvStore instance for a specific domain
16// domain: the name of the domain using this store (e.g., "pool", "position")
17func NewKVStore(domainAddress address) KVStore {
18 return &kvStore{
19 data: make(map[string]any),
20 authorizedCallers: map[address]Permission{
21 domainAddress: Write,
22 },
23 domainAddress: domainAddress,
24 }
25}
26
27// GetDomainAddress returns the domain address
28func (k *kvStore) GetDomainAddress() address {
29 return k.domainAddress
30}
31
32// GetAllKeys returns all keys stored in this kvStore
33func (k *kvStore) GetAllKeys() ([]string, error) {
34 keys := make([]string, 0, len(k.data))
35
36 // Keys are namespace-prefixed (domainAddress:key) by design
37 for key := range k.data {
38 keys = append(keys, key)
39 }
40
41 return keys, nil
42}
43
44// Has checks if a key exists in the store
45func (k *kvStore) Has(key string) bool {
46 _, exists := k.data[k.makeKey(key)]
47
48 return exists
49}
50
51// Get retrieves a value by key.
52// Reads are public within the package; only writes are gated by the ACL.
53func (k *kvStore) Get(key string) (any, error) {
54 value, exists := k.data[k.makeKey(key)]
55 if !exists {
56 return nil, ErrKeyNotFound
57 }
58
59 return value, nil
60}
61
62// GetInt64 retrieves a value by key and casts it to int64
63// Returns ErrFailedCast if the value is not of type int64
64func (k *kvStore) GetInt64(key string) (int64, error) {
65 result, err := k.Get(key)
66 if err != nil {
67 return 0, err
68 }
69
70 return castToInt64(result)
71}
72
73// GetUint64 retrieves a value by key and casts it to uint64
74// Returns ErrFailedCast if the value is not of type uint64
75func (k *kvStore) GetUint64(key string) (uint64, error) {
76 result, err := k.Get(key)
77 if err != nil {
78 return 0, err
79 }
80
81 return castToUint64(result)
82}
83
84// GetBool retrieves a value by key and casts it to bool
85// Returns ErrFailedCast if the value is not of type bool
86func (k *kvStore) GetBool(key string) (bool, error) {
87 result, err := k.Get(key)
88 if err != nil {
89 return false, err
90 }
91
92 return castToBool(result)
93}
94
95// GetString retrieves a value by key and casts it to string
96// Returns ErrFailedCast if the value is not of type string
97func (k *kvStore) GetString(key string) (string, error) {
98 result, err := k.Get(key)
99 if err != nil {
100 return "", err
101 }
102
103 return castToString(result)
104}
105
106// GetAddress retrieves a value by key and casts it to address
107// Returns ErrFailedCast if the value is not of type address
108func (k *kvStore) GetAddress(key string) (address, error) {
109 result, err := k.Get(key)
110 if err != nil {
111 return address(""), err
112 }
113
114 return castToAddress(result)
115}
116
117// GetBPTree retrieves a value by key and casts it to *bptree.BPTree
118// Returns ErrFailedCast if the value is not of type *bptree.BPTree
119func (k *kvStore) GetBPTree(key string) (*bptree.BPTree, error) {
120 result, err := k.Get(key)
121 if err != nil {
122 return nil, err
123 }
124
125 return castToBPTree(result)
126}
127
128// Set stores a value with the given key.
129//
130// rlm.IsCurrent() must hold (rejects spoofed/stale realm tokens). When the
131// caller is code, rlm.Address() -- the caller realm -- must be in
132// the authorized writers set. EOA / user-realm callers (IsCode == false)
133// bypass the ACL so deploy and test flows can populate state without prior
134// registration.
135func (k *kvStore) Set(_ int, rlm realm, key string, value any) error {
136 if !rlm.IsCurrent() {
137 return ErrSpoofedRealm
138 }
139
140 if rlm.IsCode() && !k.IsWriteAuthorized(rlm.Address()) {
141 return ErrWritePermissionDenied
142 }
143
144 k.data[k.makeKey(key)] = value
145
146 return nil
147}
148
149// Delete removes a key from the store.
150//
151// Unlike Set, Delete has no IsCode bypass: rlm.Address() is always
152// consulted against the ACL. Delete is destructive, so the relaxed path Set
153// offers for EOA / test callers is intentionally not extended here.
154func (k *kvStore) Delete(_ int, rlm realm, key string) error {
155 if !rlm.IsCurrent() {
156 return ErrSpoofedRealm
157 }
158
159 caller := rlm.Address()
160
161 if !k.IsWriteAuthorized(caller) {
162 return ErrWritePermissionDenied
163 }
164
165 if !k.Has(key) {
166 return ErrKeyNotFound
167 }
168
169 delete(k.data, k.makeKey(key))
170
171 return nil
172}
173
174// IsDomainAddress checks if the given address is the domain address
175func (k *kvStore) IsDomainAddress(addr address) bool {
176 return k.domainAddress == addr
177}
178
179// IsWriteAuthorized checks if the caller has write permission
180func (k *kvStore) IsWriteAuthorized(caller address) bool {
181 if k.IsDomainAddress(caller) {
182 return true
183 }
184
185 if !k.isRegisteredAuthorizedCaller(caller) {
186 return false
187 }
188
189 return k.authorizedCallers[caller] >= Write
190}
191
192// GetAuthorizedCallers returns all authorized callers and their permissions
193func (k *kvStore) GetAuthorizedCallers() (map[address]Permission, error) {
194 if k.authorizedCallers == nil {
195 return make(map[address]Permission), ErrAuthorizedCallerNotFound
196 }
197
198 return k.authorizedCallers, nil
199}
200
201// AddAuthorizedCaller adds a new authorized caller with the specified permission
202func (k *kvStore) AddAuthorizedCaller(_ int, rlm realm, caller address, permission Permission) error {
203 if !rlm.IsCurrent() {
204 return ErrSpoofedRealm
205 }
206
207 if !k.isUpdatableAuthorizedCaller(rlm.Address()) {
208 return ErrUpdatePermissionDenied
209 }
210
211 if k.isRegisteredAuthorizedCaller(caller) {
212 return ErrAuthorizedCallerAlreadyRegistered
213 }
214
215 if !isValidPermission(permission) {
216 return ErrInvalidPermission
217 }
218
219 k.authorizedCallers[caller] = permission
220
221 return nil
222}
223
224// UpdateAuthorizedCaller updates the permission of an existing authorized caller
225func (k *kvStore) UpdateAuthorizedCaller(_ int, rlm realm, caller address, permission Permission) error {
226 if !rlm.IsCurrent() {
227 return ErrSpoofedRealm
228 }
229
230 if !k.isUpdatableAuthorizedCaller(rlm.Address()) {
231 return ErrUpdatePermissionDenied
232 }
233
234 if !k.isRegisteredAuthorizedCaller(caller) {
235 return ErrAuthorizedCallerNotFound
236 }
237
238 if !isValidPermission(permission) {
239 return ErrInvalidPermission
240 }
241
242 k.authorizedCallers[caller] = permission
243
244 return nil
245}
246
247// RemoveAuthorizedCaller removes an authorized caller
248func (k *kvStore) RemoveAuthorizedCaller(_ int, rlm realm, caller address) error {
249 if !rlm.IsCurrent() {
250 return ErrSpoofedRealm
251 }
252
253 if !k.isUpdatableAuthorizedCaller(rlm.Address()) {
254 return ErrUpdatePermissionDenied
255 }
256
257 if !k.isRegisteredAuthorizedCaller(caller) {
258 return ErrAuthorizedCallerNotFound
259 }
260
261 delete(k.authorizedCallers, caller)
262
263 return nil
264}
265
266// isRegisteredAuthorizedCaller checks if a caller is registered
267func (k *kvStore) isRegisteredAuthorizedCaller(caller address) bool {
268 _, exists := k.authorizedCallers[caller]
269
270 return exists
271}
272
273// isUpdatableAuthorizedCaller checks if the current realm is the same as the domain address
274func (k *kvStore) isUpdatableAuthorizedCaller(currentRealmAddress address) bool {
275 return currentRealmAddress == k.domainAddress
276}
277
278// makeKey creates a prefixed key with the domain address to ensure isolation
279func (k *kvStore) makeKey(key string) string {
280 return string(k.domainAddress) + ":" + key
281}
282
283// isValidPermission ensures only Write is assignable via registration APIs.
284// The zero value is rejected so callers must spell out an explicit permission.
285func isValidPermission(permission Permission) bool {
286 return permission == Write
287}